Planteo del problema

En este caso vamos a tratar de resolver el problema de predecir si una persona que viajaba a bordo del Titanic sobrevivió o no. En particular, la regresión logística es útil para problemas de predicción de clases.

Queremos estimar \(P(Survived=1|X)=P(X)\) para cada individuo y a partir de ello poder definir un punto de corte para predecir quiénes son los que van a sobrevivir y los que no.

Para ello, utilizaremos el conjunto de datos que proviene de Kaggle - Titanic: Machine Learning from Disaster.

# Cargamos las librerías que vamos a utilizar
library(tidyverse)
library(tidymodels)
library(modelr)
library(GGally)
library(pROC)
library(cowplot)
library(OneR)
library(rlang)
library(caret)
set.seed(2021)
# cargamos los datasets de train y test 
dftitanic_train <- read_csv("../Fuentes/titanic_complete_train.csv")
dftitanic_test <- read_csv("../Fuentes/titanic_complete_test.csv")
# observamos su estructura
glimpse(dftitanic_train)
Rows: 891
Columns: 12
$ PassengerId <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,~
$ Survived    <dbl> 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0,~
$ Pclass      <dbl> 3, 1, 3, 1, 3, 3, 1, 3, 3, 2, 3, 1, 3, 3, 3, 2, 3, 2, 3, 3, 2,~
$ Name        <chr> "Braund, Mr. Owen Harris", "Cumings, Mrs. John Bradley (Floren~
$ Sex         <chr> "male", "female", "female", "female", "male", "male", "male", ~
$ Age         <dbl> 22.00000, 38.00000, 26.00000, 35.00000, 35.00000, 26.50759, 54~
$ SibSp       <dbl> 1, 1, 0, 1, 0, 0, 0, 3, 0, 1, 1, 0, 0, 1, 0, 0, 4, 0, 1, 0, 0,~
$ Parch       <dbl> 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 0, 0, 5, 0, 0, 1, 0, 0, 0, 0,~
$ Ticket      <chr> "A/5 21171", "PC 17599", "STON/O2. 3101282", "113803", "373450~
$ Fare        <dbl> 7.2500, 71.2833, 7.9250, 53.1000, 8.0500, 8.4583, 51.8625, 21.~
$ Cabin       <chr> NA, "C85", NA, "C123", NA, NA, "E46", NA, NA, NA, "G6", "C103"~
$ Embarked    <chr> "S", "C", "S", "S", "S", "Q", "S", "S", "S", "C", "S", "S", "S~
glimpse(dftitanic_test)
Rows: 418
Columns: 12
$ PassengerId <dbl> 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 90~
$ Pclass      <dbl> 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 1, 1, 2, 1, 2, 2, 3, 3, 3, 1,~
$ Name        <chr> "Kelly, Mr. James", "Wilkes, Mrs. James (Ellen Needs)", "Myles~
$ Sex         <chr> "male", "female", "male", "male", "female", "male", "female", ~
$ Age         <dbl> 34.50000, 47.00000, 62.00000, 27.00000, 22.00000, 14.00000, 30~
$ SibSp       <dbl> 0, 1, 0, 0, 1, 0, 0, 1, 0, 2, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,~
$ Parch       <dbl> 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,~
$ Ticket      <chr> "330911", "363272", "240276", "315154", "3101298", "7538", "33~
$ Fare        <dbl> 7.8292, 7.0000, 9.6875, 8.6625, 12.2875, 9.2250, 7.6292, 29.00~
$ Cabin       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "B45", NA, "E3~
$ Embarked    <chr> "Q", "S", "Q", "S", "S", "S", "Q", "S", "C", "S", "S", "S", "S~
$ Survived    <dbl> 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0,~

El dataset de training tiene 12 variables y 891 observaciones, mientras que el de testing tiene 418.

Las variables del set incluyen id, nombre, edad, sexo del pasajero, como también:

  • Survived: La clase que queremos predecir: Sobrevivió (0 = No, 1 = Yes).
  • Pclass: La clase a la que pertenece el pasajero (1 = 1st, 2 = 2nd, 3 = 3rd). Es un proxy del estatus socio-económico (1st = Upper, 2nd = Middle, 3rd = Lower).
  • Sibsp: número de hermanos / cónyuges a bordo del Titanic.
  • Parch: número de padres / hijos a bordo del Titanic.
  • Ticket: número de Ticket
  • Fare: tarifa.
  • Cabin: número de cabina
  • Embarked: Puerto de embarque (C = Cherbourg, Q = Queenstown, S = Southampton).

Se transforman las variables Pclass y Embarked a factor.

dftitanic_train <- dftitanic_train %>%
  mutate(Pclass = factor(Pclass), Embarked = factor(Embarked))
dftitanic_test <- dftitanic_test %>%
  mutate(Pclass = factor(Pclass), Embarked = factor(Embarked))

Análisis Exploratorios

Analicemos la distribución de la clase en cada dataset.

# calculamos la distribución de clase en cada dataset
train <- dftitanic_train %>% 
  group_by(Survived) %>% 
  summarise(numero_casos=n()) %>%
  mutate(prop = round(prop.table(numero_casos)*100,2))
test <- dftitanic_test %>% 
  group_by(Survived) %>% 
  summarise(numero_casos=n()) %>%
  mutate(prop = round(prop.table(numero_casos)*100,2))
# armamos tabla conjunta para graficar
distrib = cbind(rbind(train, test), dataset = c("train", "train", "test", "test"))
distrib
# graficamos las distribuciones
ggplot(distrib, aes(x = Survived, y = prop, fill = factor(Survived), label = prop)) + 
         geom_bar(stat="identity", position = "dodge") + facet_wrap(~ dataset) +
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
  labs(x = "Sobrevivientes", y = "Proporción en %", title = "Proporción de sobrevivientes por dataset") + 
  theme_bw() +
  scale_fill_brewer(palette="Set1")

Vemos que estamos trabajando con un problema de clasificación con cierto desbalance de clase, pero que la proporción se mantiene en ambos conjuntos de datos.

Realizamos un gráfico exploratorio completo para ver el comportamiento y las relaciones entre las variables. El color rojo designa a quienes no sobrevivieron y el azul a los que sí.

# graficamos con ggpairs coloreando por variable a predecir
g <- dftitanic_train %>% 
        select("Survived","Pclass", "Sex", "Age", "Fare", "SibSp", "Parch") %>% 
        ggpairs(title = "Correlograma de variables",
                mapping = aes(colour= factor(Survived)),
                progress = FALSE, 
                lower=list(combo=wrap("facethist", binwidth=0.8))) +
        theme(axis.text.x = element_text(angle = 90, hjust = 1)) + 
        theme_bw() +
        scale_fill_brewer(palette="Set1") +
        scale_color_brewer(palette="Set1")
g

¿Qué pueden decir de la relación entre clase (Pclass) y supervivencia (Survived)?

¿Y entre edad (Age) y supervivencia?

¿Cuáles parecen ser buenas variables para discriminar entre quienes sobrevivieron y quienes no?

Probemos algunas soluciones

Regresión lineal

En este caso estamos modelando la probabilidad de la siguiente manera:

\(P(X)= \beta_0 + \sum\limits_{j=1}^p \beta_j X\)

Veamos que tan bueno es el modelo lineal para esto, usando la edad como predictor.

mrl <- dftitanic_train %>% 
              lm(formula = Survived ~ Age) 
tdy = mrl %>% tidy() 
tdy
mrl %>% glance()

Los estimadores son significativos y el test de significatividad global del modelo también es significativo.

Veamos un gráfico de nuestro modelo.

Parece tener bastantes problemas para estimar la probabilidad de supervivencia de los individuo: no existe un punto de corte claro, la predicción podría ser mayor a 1 o menor a cero llegado el caso.

Regresión Logística

Para evitar estos problemas, usamos la función logística.

\(P(Y=1|X)= \frac{e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}{1+e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}\)

El lado derecho se llama expit

Esta función acota el resultado entre 0 y 1, lo cual es mucho más adecuado para modelar una probabilidad.

Luego de hacer algunas operaciones, podemos llegar a la expresión:

\(\log {\frac{P(x)}{1-P(x)}}= \beta_0 + \sum\limits_{j=1}^p \beta_j X\)

El lado izquierdo es el logaritmo de los odds y se llama logit.

Modelo

La funcíón glm() nos permite crear un modelo lineal generalizado (Generalized Linear Model). Al igual que la función lm() toma como argumentos una formula y los datos pero también se debe especificar el argumento family: indicamos la distribución del error y la función link que vamos a utilizar en el modelo.

Algunas familias son:

  • Binomial: link=logit

  • Poisson: link=log

  • Gaussiana: link=identidad

Como estamos trabajando con un fenómeno que suponemos tiene una distribución binomial, así lo especificamos en el parámetro family.

Realizamos un modelo de regresión logística para predecir la supervivencia en función de Pclass, Sex y Age.

# modelo de regresión logística 
glm1 <- glm(data = dftitanic_train, Survived ~ Pclass + Sex + Age, family = 'binomial')
# veo los resultados
tidy(glm1)
glance(glm1)

Se va a discutir sobre la interpretación de coeficientes y la evaluación en las siguientes secciones.

Creación de fórmulas

Para crear varios modoelos de regresión logística podemos utilizar la función formulas del paquete modelr para crear un objeto que contiene todas las fórmulas que vamos a utilizar.

En .response especificamos la variable respuesta de nuestras fórmulas y luego nombramos las fórmulas que queramos armar.

Así, armaremos distintos modelos combinando distintas variables. Se generan 7 modelos distintos para predecir la supervivencia en función de distintas combinaciones de las variables pclass, sex, age y fare.

# Creación de fórmulas
logit_formulas <- formulas(.response = ~ Survived,
                           class = ~ Pclass, 
                           sex = ~ Sex, 
                           age = ~ Age,  
                           PcS = ~ Pclass + Sex, # modelo con las variables que más parecen dividir el target
                           PcSA = ~ Pclass + Sex + Age, # modelo glm1
                           PcSF = ~ Pclass + Sex + Fare,  # modelo previo sin Age
                           PcSAF = ~ Pclass + Sex + Age + Fare # modelo glm1 con Fare  
                           )
logit_formulas # observamos el objeto formulas
$class
Survived ~ Pclass

$sex
Survived ~ Sex

$age
Survived ~ Age

$PcS
Survived ~ Pclass + Sex

$PcSA
Survived ~ Pclass + Sex + Age

$PcSF
Survived ~ Pclass + Sex + Fare

$PcSAF
Survived ~ Pclass + Sex + Age + Fare

Creación de modelos

Procedemos a crear los modelos a partir de estas fórmulas.

models <- data_frame(logit_formulas) %>% # dataframe a partir del objeto formulas
  mutate(models = names(logit_formulas), # columna con los nombres de las formulas
         expression = paste(logit_formulas), # columna con las expresiones de las formulas
         mod = map(logit_formulas, ~glm(., family = 'binomial', data = dftitanic_train))) # Que estamos haciendo acá? Que vamos a encontrar en la columna?
models

Modelos simples

Probamos los primeros tres modelos, aquellos que tienen un único predictor. Usamos la función tidy para obtener los parámetros estimados para estos tres modelos.

models %>% 
  filter(models %in% c('class','sex','age')) %>%
  mutate(tidy = map(mod, tidy))  # Qué realizamos en este paso? Que va a tener esta columna?

Para acceder a los elementos de la nueva columna tidy debemos desanidarla (usando unnest()). El anidado crea una columna de listas de dataframes, es implícitamente una operación de resumen: obtiene una fila para cada grupo definido por las columnas no anidadas. Desanidar lo aplana de nuevo en columnas regulares. Para mayor detalle, sugerimos ver la documentación de tidyr sobre Nest and unnest.

models %>% 
  filter(models %in% c('class','sex','age')) %>%
  mutate(tidy = map(mod, tidy)) %>%  # Qué realizamos en este paso? Que va a tener esta columna?
  unnest(tidy) %>% 
  mutate(estimate=round(estimate,5), # redondeamos valores para facilitar lectura
         p.value=round(p.value,4))

Observamos que todos los modelos tienen coeficientes significativos aunque el de Age se encuentra muy cercano a nuestro valor de rechazo.

Interpretación de los coeficientes

Recordando la ecuación para modelar la probabilidad:

\(P(Y=1|X)= \frac{e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}{1+e^{\beta_0 + \sum\limits_{j=1}^p \beta_j X}}\)

Se observa que ahora las variables ya no tienen una relación lineal con la probabilidad. En este modelo un coeficiente positivo indica que frente a aumentos de dicha variable la probabilidad aumenta, mientras que un coeficiente negativo nos indica lo contrario. Para nuestros modelos:

Modelo class

  • β0 = 0.53063 corresponde a personas que viajaron en primera clase.

  • β1 = -0.63943 y β2 = -1.6704 corresponden a personas que viajaron en la segunda y tercera clase, respectivamente. El coeficiente estimado en ambos casos fue negativo, indicando que la probabilidad de supervivencia disminuye en comparación con la primera clase. Al ser más negativo el coeficiente estimado de Pclass3, se espera que la probabilidad de Supervivencia sea menor aún para esta clase que para un pasajero en clase 2.

Modelo sex

  • β0 = 1.05659 corresponde a mujeres que viajaron a bordo del titanic.

  • β1 = -2.51371 representa a los hombres que viajaron en el titanic e indica que la probabilidad de supervivencia se reduce en comparación a las mujeres.

Modelo age

  • β1 = -0.0106 indica que la probabilidad de supervivencia disminuye por cada año más de edad de la persona.

¿Qué pasaría en un modelo múltiple?

models %>% 
  filter(models %in% c('PcSA')) %>%
  mutate(tidy = map(mod, tidy)) %>% 
  unnest(tidy) %>% 
  mutate(estimate=round(estimate,5),
         p.value=round(p.value,4))

Así como se observó en el modelo simple para la variable Pclass, en este caso los coeficientes estimados de Pclass2 y Pclass3 resultan negativos, indicando que si el pasajero pertenece a segunda o tercera clase, su probabilidad de superviviencia esperada disminuye respecto de una persona de primera clase, dadas las demás variables del modelo (Sex y Age).

Para la variable Sex observamos que el coeficiente SexMale resulta negativo; esto significa que la probabilidad de supervivencia esperada de los hombres disminuye respecto a la de las mujeres, dadas las demás variables del modelo.

Para la variable Age, el coeficiente estimado también resulta negativo, indicando que ante un aumento en la edad del pasajero, su probabilidad de supervivencia esperada disminuye, dadas las demás variables.

Asimismo, se observa que los coeficientes resultan todos significativos en este modelo (p-valor<0.05).

Recomendamos leer el capítulo de regresión logística de Interpretable Machine Learning: A Guide for Making Black Box Models Explainable de Molnar Cristoph para una discusión más profunda de la interpretación de un modelo de regresión logística.

Evaluación de todos los modelos

Con map() agregamos la función glance para traer información relevante para la evaluación del modelo. Con unnest() accedemos a dicha información. Por último, agregamos una columna con el porcentaje de deviance explicado por cada modelo y ordenamos el dataset según su valor de deviance.

# Calcular las medidas de evaluación para cada modelo
models <- models %>% 
  mutate(glance = map(mod,glance))
# Obtener las medidas de evaluacion de interes
models %>% 
  unnest(glance) %>%
  # Calculamos la deviance explicada
  mutate(perc_explained_dev = 1-deviance/null.deviance) %>% 
  select(-c(models, df.null, AIC, BIC)) %>% 
  arrange(deviance)

Los modelos que incluyen las 3 variables Pclass, Sex y Age parecen ser los que minimizan la deviance. Observamos también que los 2 últimos modelos reducen muy poco la deviance respecto a la deviance nula.

Gráficos de Evaluación

Realizamos los gráficos para el modelo completo y uno de los modelos con mayor deviance (Age).

Comenzamos agregando las predicciones con augment con el parámetro type="response". La función augment hereda el argumento type.predict de la función predict.

  • Si type.predict = 'link' la predicción es en términos de la función link. En nuestro caso son el logaritmo de las odds, es decir, los valores que toma la expresión logit.

  • Si type.predict = 'response' la predicción son las probabilidades de que la observación pertenezca a la clase positiva. En nuestro caso, devuelve la probabilidad de la que persona sobreviva.

# Añadir las predicciones
models <- models %>% 
  mutate(pred= map(mod, augment, type.predict = "response"))
#Observaciones con probabilidad más baja
models$pred$PcSAF %>% arrange(.fitted) %>% head(10)
#Observaciones con probabilidad más alta
models$pred$PcSAF %>% arrange(desc(.fitted)) %>% head(10)

Guardamos las predicciones para los modelos mencionados.

# Modelo completo
prediction_full <- models %>% 
  filter(models=="PcSAF") %>% 
  unnest(pred)
#Modelo malo
prediction_bad <- models %>% 
  filter(models=="age") %>% 
  unnest(pred)

Violin plots

# graficamos el modelo completo
violin_full = ggplot(prediction_full, aes(x=Survived, y=.fitted, group=Survived, fill=factor(Survived))) + 
  geom_violin() +
  theme_bw() +
  guides(scale="none") +
  labs(title='Violin plot', subtitle='Modelo completo', y='Predicted probability')
# graficamos el modelo malo
violin_bad = ggplot(prediction_bad, aes(x=Survived, y=.fitted, group=Survived, fill=factor(Survived))) + 
  geom_violin() + 
  theme_bw() +
  guides(scale="none") +
  labs(title='Violin plot', subtitle='Modelo malo', y='Predicted probability')
# mostramos ambos
plot_grid(violin_bad, violin_full)

En los gráficos de violin observamos:

  • En el eje de abscisas la clase verdadera: Survived o No Survived (1 o 0).

  • En el eje de ordenadas la probabilidad predicha por nuestro modelo.

  • El gráfico nos muestra la distribución de la cantidad de observaciones por su clase real y la probabilidad que le asigna nuestro modelo.

¿Cuál parece ser un punto de corte adecuado para cada modelo?

Gráfico de Hosmer-Lemeshow

Se genera una función para realizar un gráfico de Hosmer-Lemeshow para un dataset. Para ello se fijan los siguientes parámetros:

  • dataset: conjunto de datos

  • predicted_column: columna con la probabilidad predicha

  • class_column: columna con la clase a predecir

  • possitive_value: valor de la clase a predecir

  • bins: cantidad de grupos del gráfico

  • color: color de los puntos

  • nudge_x: desplazamiento de la etiqueta en el eje x

  • nudge_y: desplazamiento de la etiqueta en el eje y

Hosmer_Lemeshow_plot <- function(dataset, predicted_column, class_column, bins, positive_value, color='forestgreen', nudge_x=0, nudge_y=0.05){
  # Asignar los grupos a las observaciones de acuerdo a la probabilidad predicha
  dataset['group'] <- bin(dataset[predicted_column], nbins = bins, method = 'l', labels=c(1:bins))
  # Contar la cantidad de casos positivos por grupo
  positive_class <- dataset %>% filter(!!sym(class_column)==positive_value) %>% group_by(group) %>% count()
  # Obtener la media de las predicciones por grupo
  HL_df <- dataset %>% group_by(group) %>% summarise(pred=mean(!!sym(predicted_column)), count=n()) %>%
            inner_join(.,positive_class) %>%
            mutate(freq=n/count)
  # Gráfico 
  HM_plot <- ggplot(HL_df, aes(x=pred, y=freq)) + 
    geom_point(aes(size=n), color=color) +
    geom_text(aes(label=n),nudge_y = nudge_y)+
    geom_abline(slope = 1, intercept = 0, linetype='dashed') + 
    theme_bw() +
    labs(title='Hosmer-Lemeshow', size='Casos', x="Probabilidad Predicha", y="Frecuencia observada")
  return(HM_plot)
}

Generamos los gráficos pasandole lo parámetros.

# modelo completo
Hosmer_Lemeshow_plot(prediction_full, '.fitted', 'Survived', 10, 1) +
  labs(subtitle="Modelo completo")
Joining, by = "group"

# modelo malo
Hosmer_Lemeshow_plot(prediction_bad, '.fitted', 'Survived', 10, 1, color = "firebrick") + labs(subtitle="Modelo malo")
Joining, by = "group"

En los gráficos de Hosmer-Lemeshow observamos:

  • En el eje de abscisas la probabilidad predicha de supervivencia.

  • En el eje de ordenadas la frecuencia de clase, el cociente entre cantidad de individuos Survived y el total de individuos.

  • La línea punteada designa la igualdad entre probabilidad predicha y frecuencia de clase.

  • Los círculos, que se construyen de la siguiente manera:

    • Se dividen a las observaciones en bins en base a la probabilidad predicha
    • Se calcula la frecuencia de clase para cada bin
    • En base a estas dos coordenadas se ubica al círculo en el gráfico
    • El número y tamaño indican la cantidad de observaciones en dicho grupo

Aquellos círculos que se ubiquen por encima de la línea punteada indican que el modelo está subestimando la probabilidad para dichos grupos. Mientras que si los círculos se ubican por debajo el modelo está sobreestimando la probabilidad para dichos grupos.

¿Para qué valores parece existir una sobreestimación de la probabilidad? ¿Para cuáles subestimación?

Curvas ROC

# Calculamos curvas ROC
roc_full <- roc(response=prediction_full$Survived, predictor=prediction_full$.fitted)
roc_bad <- roc(response=prediction_bad$Survived, predictor=prediction_bad$.fitted)

Graficamos ambas en un mismo plot.

ggroc(list(full=roc_full, bad=roc_bad), size=1) + 
  geom_abline(slope = 1, intercept = 1, linetype='dashed') +
  theme_bw() + 
  labs(title='Curvas ROC', color='Modelo')

print(paste('AUC: Modelo completo', round(roc_full$auc,3)))
[1] "AUC: Modelo completo 0.848"
print(paste('AUC: Modelo malo', round(roc_bad$auc,3)))
[1] "AUC: Modelo malo 0.474"

¿Qué significa cada uno de los ejes?

Punto de corte

Hasta ahora hemos evaluado el modelo de manera general, pero el resultado final del modelo debe consistir en asignar a la persona una clase predicha. En nuestro caso debemos establecer un punto de corte según el cual vamos a separar a las personas en quienes sobreviven y quienes no.

Probamos varios puntos de corte y graficamos el accuracy, la sensibilidad o recall, la especificidad y la precisión para cada uno de ellos.

Clases predichas / Clases Negativa Positiva
Negativa True Neg False Neg
Positiva False Pos True Pos

Recordemos que:

\(accuracy = \frac{TP+TN}{TP+FP+FN+TN}\)

\(sensitivity = recall = \frac{TP}{TP+FN}\)

\(specificity = \frac{TN}{TN+FP}\)

\(precision = \frac{TP}{TP+FP}\)

prediction_metrics <- function(cutoff, predictions=prediction_full){
  tab <- predictions %>% 
    mutate(predicted_class = if_else(.fitted > cutoff, 1, 0),
           Survived = factor(Survived))
  confusionMatrix(table(tab$predicted_class, tab$Survived), positive = "1") %>%
    tidy() %>%
    select(term, estimate) %>%
    filter(term %in% c('accuracy', 'sensitivity', 'specificity', 'precision')) %>%
    mutate(cutoff = cutoff)
}
cutoffs = seq(0.05,0.95,0.01)
logit_pred = map_df(cutoffs, prediction_metrics) %>% 
  mutate(term = as.factor(term), estimate = round(estimate, 3))
ggplot(logit_pred, aes(cutoff,estimate, group=term, color=term)) + geom_line(size=1) +
  theme_bw() +
  labs(title= 'Accuracy, Sensitivity, Specificity y Precision', subtitle= 'Modelo completo', color="")

¿Qué podemos observar en el gráfico?

¿Podemos definir un buen punto de corte? ¿Cuál sería?

¿Por qué la especificidad tiene ese comportamiento?

Dataset de testing

Seleccionamos el modelo completo, ya que es el que maximizaba el porcentaje de deviance explicada y en base a lo que vimos definimos un punto de corte en 0.4 (pueden probar otros), donde se cruzan sensitivity y specificity.

Calculamos la matriz de confusión para los datasets de train y test.

# Creamos la matriz de confusión
confusionMatrix(table(table_train$predicted_class, table_train$Survived), positive = "1")
Confusion Matrix and Statistics

   
      0   1
  0 427  68
  1 122 274
                                          
               Accuracy : 0.7868          
                 95% CI : (0.7584, 0.8132)
    No Information Rate : 0.6162          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.5622          
                                          
 Mcnemar's Test P-Value : 0.0001205       
                                          
            Sensitivity : 0.8012          
            Specificity : 0.7778          
         Pos Pred Value : 0.6919          
         Neg Pred Value : 0.8626          
             Prevalence : 0.3838          
         Detection Rate : 0.3075          
   Detection Prevalence : 0.4444          
      Balanced Accuracy : 0.7895          
                                          
       'Positive' Class : 1               
                                          
# Agregamos la predicciones al dataset de testeo
table_test = augment(x = full_model, newdata=dftitanic_test, type.predict='response') 
# Clasificamos utilizamos el punto de corte
table_test = table_test %>% 
  mutate(predicted_class = if_else(.fitted>sel_cutoff, 1, 0) %>% as.factor(), 
         Survived = factor(Survived))
# Creamos la matriz de confusión
confusionMatrix(table(table_test$predicted_class, table_test$Survived), positive = "1")
Confusion Matrix and Statistics

   
      0   1
  0 191  39
  1  70 118
                                          
               Accuracy : 0.7392          
                 95% CI : (0.6943, 0.7807)
    No Information Rate : 0.6244          
    P-Value [Acc > NIR] : 4.341e-07       
                                          
                  Kappa : 0.4651          
                                          
 Mcnemar's Test P-Value : 0.00406         
                                          
            Sensitivity : 0.7516          
            Specificity : 0.7318          
         Pos Pred Value : 0.6277          
         Neg Pred Value : 0.8304          
             Prevalence : 0.3756          
         Detection Rate : 0.2823          
   Detection Prevalence : 0.4498          
      Balanced Accuracy : 0.7417          
                                          
       'Positive' Class : 1               
                                          

Al existir desbalanceo de clases, es posible que el analista desee saber si la precisión general (Accuracy) de su modelo es mejor que la proporción de datos con la clase mayoritaria (No-information Rate). confusionMatrix usa la función binom.test para probar que la precisión (Acc) es mejor que la tasa sin información (NIR). Si el P-Value [Acc > NIR] resulta significativo, entonces podemos decir que existe evidencia estadísticamente significativa para decir que la precisión del modelo es mejor que la tasa sin información.

Desbalanceo de la clase

Al explorar el dataset vimos que existía cierto desbalance de clase. Esto puede tener un efecto en las estimaciones del modelo y su clasificación final.

Existen dos maneras sencillas con las cuales podemos trabajar con una clase desbalanceada:

  • Sobre-muestreo (oversampling) de la clase minoritaria

  • Sub-muestreo (undersampling) de la clase mayoritaria

La función glm puede tomar como argumento una columna (weigths) de ponderadores para poder hacer esto. Podemos asignar pesos mayores a 1 a la clase minoritaria (oversampling) o menores a 1 a la clase mayoritaria (undersampling). En nuestro problema vamos a realizar un sobresampleo de la clase minoritaria.

# Creamos la columna de ponderadores
train <- dftitanic_train %>% mutate(wt = if_else(Survived == 1, 1.1, 1))
# Creamos los modelos con la data 'balanceada'
balanced_models <- data_frame(logit_formulas) %>% # dataframe a partir del objeto formulas
  mutate(models = names(logit_formulas), # columna con los nombres de las formulas
         expression = paste(logit_formulas), # columna con las expresiones de las formulas
         mod = map(logit_formulas, ~glm(., family = 'binomial', data = train, weights = wt))) #Pasamos la columna wt como ponderadores

Vemos las estimaciones de los parámetros para el modelo completo. ¿Existen cambios?

Ahora veamos la evaluación de los modelos ¿Qué pasó con el porcentaje de deviance explicada? ¿Y con la nula?

Setting levels: control = 0, case = 1
Setting direction: controls < cases
Setting levels: control = 0, case = 1
Setting direction: controls > cases

Violin plots, Curvas ROC y AUCs

Realizamos los gráficos de violin, las curvas ROC y calculamos las AUC.

[1] "AUC Modelo completo: 0.848"
[1] "AUC Modelo malo: 0.474"

¿Dónde se ven los cambios más notorios respecto a nuestros modelos anteriores que no tenían en cuenta el desbalance de la clase?

Punto de corte

Volvemos a realizar las pruebas para varios puntos de corte y graficamos el accuracy, la sensibilidad, la especificidad, el recall y la precision para cada uno de ellos.

¿Qué cambios vemos respecto al gráfico anterior?

Dataset de testing

Probamos en el dataset de testing nuestro modelo balanceado. No es necesario que le creemos pesos al dataset de testeo.

Warning in eval(family$initialize) :
  non-integer #successes in a binomial glm!
Confusion Matrix and Statistics

   
      0   1
  0 195  42
  1  66 115
                                          
               Accuracy : 0.7416          
                 95% CI : (0.6968, 0.7829)
    No Information Rate : 0.6244          
    P-Value [Acc > NIR] : 2.496e-07       
                                          
                  Kappa : 0.4654          
                                          
 Mcnemar's Test P-Value : 0.02689         
                                          
            Sensitivity : 0.7325          
            Specificity : 0.7471          
         Pos Pred Value : 0.6354          
         Neg Pred Value : 0.8228          
             Prevalence : 0.3756          
         Detection Rate : 0.2751          
   Detection Prevalence : 0.4330          
      Balanced Accuracy : 0.7398          
                                          
       'Positive' Class : 1               
                                          
LS0tDQp0aXRsZTogIlJlZ3Jlc2nDs24gTG9nw61zdGljYSINCmF1dGhvcjogIkp1YW4gQmFycmlvbGEgeSBTb2bDrWEgUGVyaW5pIg0KZGF0ZTogIjMwIGRlIE9jdHVicmUgZGUgMjAyMSINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0aGVtZTogc3BhY2VsYWINCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQotLS0NCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQpkaXYubWFpbi1jb250YWluZXIgew0KICBtYXgtd2lkdGg6IDE2MDBweDsNCiAgbWFyZ2luLWxlZnQ6IGF1dG87DQogIG1hcmdpbi1yaWdodDogYXV0bzsNCn0NCjwvc3R5bGU+DQoNCiMjIFBsYW50ZW8gZGVsIHByb2JsZW1hDQoNCkVuIGVzdGUgY2FzbyB2YW1vcyBhIHRyYXRhciBkZSByZXNvbHZlciBlbCBwcm9ibGVtYSBkZSAqKnByZWRlY2lyIHNpIHVuYSBwZXJzb25hIHF1ZSB2aWFqYWJhIGEgYm9yZG8gZGVsIFRpdGFuaWMgc29icmV2aXZpw7MgbyBubyoqLiBFbiBwYXJ0aWN1bGFyLCBsYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgZXMgw7p0aWwgcGFyYSBwcm9ibGVtYXMgZGUgcHJlZGljY2nDs24gZGUgY2xhc2VzLg0KDQpRdWVyZW1vcyBlc3RpbWFyICRQKFN1cnZpdmVkPTF8WCk9UChYKSQgcGFyYSBjYWRhIGluZGl2aWR1byB5IGEgcGFydGlyIGRlIGVsbG8gcG9kZXIgZGVmaW5pciB1biBwdW50byBkZSBjb3J0ZSBwYXJhIHByZWRlY2lyIHF1acOpbmVzIHNvbiBsb3MgcXVlIHZhbiBhIHNvYnJldml2aXIgeSBsb3MgcXVlIG5vLg0KDQpQYXJhIGVsbG8sIHV0aWxpemFyZW1vcyBlbCBjb25qdW50byBkZSBkYXRvcyBxdWUgcHJvdmllbmUgZGUgW0thZ2dsZSAtIFRpdGFuaWM6IE1hY2hpbmUgTGVhcm5pbmcgZnJvbSBEaXNhc3Rlcl0oaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9jL3RpdGFuaWMvb3ZlcnZpZXcpLg0KDQpgYGB7ciwgZWNobz1UUlVFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBDYXJnYW1vcyBsYXMgbGlicmVyw61hcyBxdWUgdmFtb3MgYSB1dGlsaXphcg0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHRpZHltb2RlbHMpDQpsaWJyYXJ5KG1vZGVscikNCmxpYnJhcnkoR0dhbGx5KQ0KbGlicmFyeShwUk9DKQ0KbGlicmFyeShjb3dwbG90KQ0KbGlicmFyeShPbmVSKQ0KbGlicmFyeShybGFuZykNCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgyMDIxKQ0KYGBgDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KIyBjYXJnYW1vcyBsb3MgZGF0YXNldHMgZGUgdHJhaW4geSB0ZXN0IA0KZGZ0aXRhbmljX3RyYWluIDwtIHJlYWRfY3N2KCIuLi9GdWVudGVzL3RpdGFuaWNfY29tcGxldGVfdHJhaW4uY3N2IikNCmRmdGl0YW5pY190ZXN0IDwtIHJlYWRfY3N2KCIuLi9GdWVudGVzL3RpdGFuaWNfY29tcGxldGVfdGVzdC5jc3YiKQ0KYGBgDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQ0KIyBvYnNlcnZhbW9zIHN1IGVzdHJ1Y3R1cmENCmdsaW1wc2UoZGZ0aXRhbmljX3RyYWluKQ0KZ2xpbXBzZShkZnRpdGFuaWNfdGVzdCkNCmBgYA0KDQpFbCBkYXRhc2V0IGRlIHRyYWluaW5nIHRpZW5lIDEyIHZhcmlhYmxlcyB5IDg5MSBvYnNlcnZhY2lvbmVzLCBtaWVudHJhcyBxdWUgZWwgZGUgdGVzdGluZyB0aWVuZSA0MTguDQoNCkxhcyB2YXJpYWJsZXMgZGVsIHNldCBpbmNsdXllbiBpZCwgbm9tYnJlLCBlZGFkLCBzZXhvIGRlbCBwYXNhamVybywgY29tbyB0YW1iacOpbjoNCg0KKiAqKlN1cnZpdmVkKio6IExhIGNsYXNlIHF1ZSBxdWVyZW1vcyBwcmVkZWNpcjogU29icmV2aXZpw7MgKDAgPSBObywgMSA9IFllcykuDQoqICoqUGNsYXNzKio6IExhIGNsYXNlIGEgbGEgcXVlIHBlcnRlbmVjZSBlbCBwYXNhamVybyAoMSA9IDFzdCwgMiA9IDJuZCwgMyA9IDNyZCkuIEVzIHVuIHByb3h5IGRlbCBlc3RhdHVzIHNvY2lvLWVjb27Ds21pY28gKDFzdCA9IFVwcGVyLCAybmQgPSBNaWRkbGUsIDNyZCA9IExvd2VyKS4gDQoqICoqU2lic3AqKjogbsO6bWVybyBkZSBoZXJtYW5vcyAvIGPDs255dWdlcyBhIGJvcmRvIGRlbCBUaXRhbmljLg0KKiAqKlBhcmNoKio6IG7Dum1lcm8gZGUgcGFkcmVzIC8gaGlqb3MgYSBib3JkbyBkZWwgVGl0YW5pYy4JDQoqICoqVGlja2V0Kio6IG7Dum1lcm8gZGUJVGlja2V0DQoqICoqRmFyZSoqOiB0YXJpZmEuIA0KKiAqKkNhYmluKio6IG7Dum1lcm8gZGUgY2FiaW5hDQoqICoqRW1iYXJrZWQqKjogUHVlcnRvIGRlIGVtYmFycXVlIChDID0gQ2hlcmJvdXJnLCBRID0gUXVlZW5zdG93biwgUyA9IFNvdXRoYW1wdG9uKS4gDQoNClNlIHRyYW5zZm9ybWFuIGxhcyB2YXJpYWJsZXMgKipQY2xhc3MqKiB5ICoqRW1iYXJrZWQqKiBhIGZhY3Rvci4NCg0KYGBge3J9DQpkZnRpdGFuaWNfdHJhaW4gPC0gZGZ0aXRhbmljX3RyYWluICU+JQ0KICBtdXRhdGUoUGNsYXNzID0gZmFjdG9yKFBjbGFzcyksIEVtYmFya2VkID0gZmFjdG9yKEVtYmFya2VkKSkNCmRmdGl0YW5pY190ZXN0IDwtIGRmdGl0YW5pY190ZXN0ICU+JQ0KICBtdXRhdGUoUGNsYXNzID0gZmFjdG9yKFBjbGFzcyksIEVtYmFya2VkID0gZmFjdG9yKEVtYmFya2VkKSkNCmBgYA0KDQojIyBBbsOhbGlzaXMgRXhwbG9yYXRvcmlvcw0KDQpBbmFsaWNlbW9zIGxhIGRpc3RyaWJ1Y2nDs24gZGUgbGEgY2xhc2UgZW4gY2FkYSBkYXRhc2V0LiANCg0KYGBge3J9DQojIGNhbGN1bGFtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBjbGFzZSBlbiBjYWRhIGRhdGFzZXQNCnRyYWluIDwtIGRmdGl0YW5pY190cmFpbiAlPiUgDQogIGdyb3VwX2J5KFN1cnZpdmVkKSAlPiUgDQogIHN1bW1hcmlzZShudW1lcm9fY2Fzb3M9bigpKSAlPiUNCiAgbXV0YXRlKHByb3AgPSByb3VuZChwcm9wLnRhYmxlKG51bWVyb19jYXNvcykqMTAwLDIpKQ0KdGVzdCA8LSBkZnRpdGFuaWNfdGVzdCAlPiUgDQogIGdyb3VwX2J5KFN1cnZpdmVkKSAlPiUgDQogIHN1bW1hcmlzZShudW1lcm9fY2Fzb3M9bigpKSAlPiUNCiAgbXV0YXRlKHByb3AgPSByb3VuZChwcm9wLnRhYmxlKG51bWVyb19jYXNvcykqMTAwLDIpKQ0KIyBhcm1hbW9zIHRhYmxhIGNvbmp1bnRhIHBhcmEgZ3JhZmljYXINCmRpc3RyaWIgPSBjYmluZChyYmluZCh0cmFpbiwgdGVzdCksIGRhdGFzZXQgPSBjKCJ0cmFpbiIsICJ0cmFpbiIsICJ0ZXN0IiwgInRlc3QiKSkNCmRpc3RyaWINCiMgZ3JhZmljYW1vcyBsYXMgZGlzdHJpYnVjaW9uZXMNCmdncGxvdChkaXN0cmliLCBhZXMoeCA9IFN1cnZpdmVkLCB5ID0gcHJvcCwgZmlsbCA9IGZhY3RvcihTdXJ2aXZlZCksIGxhYmVsID0gcHJvcCkpICsgDQogICAgICAgICBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIsIHBvc2l0aW9uID0gImRvZGdlIikgKyBmYWNldF93cmFwKH4gZGF0YXNldCkgKw0KICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEpKSArDQogIGxhYnMoeCA9ICJTb2JyZXZpdmllbnRlcyIsIHkgPSAiUHJvcG9yY2nDs24gZW4gJSIsIHRpdGxlID0gIlByb3BvcmNpw7NuIGRlIHNvYnJldml2aWVudGVzIHBvciBkYXRhc2V0IikgKyANCiAgdGhlbWVfYncoKSArDQogIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IlNldDEiKQ0KYGBgDQoNClZlbW9zIHF1ZSBlc3RhbW9zIHRyYWJhamFuZG8gY29uIHVuIHByb2JsZW1hIGRlIGNsYXNpZmljYWNpw7NuIGNvbiBjaWVydG8gZGVzYmFsYW5jZSBkZSBjbGFzZSwgcGVybyBxdWUgbGEgcHJvcG9yY2nDs24gc2UgbWFudGllbmUgZW4gYW1ib3MgY29uanVudG9zIGRlIGRhdG9zLiANCg0KUmVhbGl6YW1vcyB1biBncsOhZmljbyBleHBsb3JhdG9yaW8gY29tcGxldG8gcGFyYSB2ZXIgZWwgY29tcG9ydGFtaWVudG8geSBsYXMgcmVsYWNpb25lcyBlbnRyZSBsYXMgdmFyaWFibGVzLiBFbCBjb2xvciByb2pvIGRlc2lnbmEgYSBxdWllbmVzIG5vIHNvYnJldml2aWVyb24geSBlbCBhenVsIGEgbG9zIHF1ZSBzw60uDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD02LCBwcm9ncmVzcz1GQUxTRX0NCiMgZ3JhZmljYW1vcyBjb24gZ2dwYWlycyBjb2xvcmVhbmRvIHBvciB2YXJpYWJsZSBhIHByZWRlY2lyDQpnIDwtIGRmdGl0YW5pY190cmFpbiAlPiUgDQogICAgICAgIHNlbGVjdCgiU3Vydml2ZWQiLCJQY2xhc3MiLCAiU2V4IiwgIkFnZSIsICJGYXJlIiwgIlNpYlNwIiwgIlBhcmNoIikgJT4lIA0KICAgICAgICBnZ3BhaXJzKHRpdGxlID0gIkNvcnJlbG9ncmFtYSBkZSB2YXJpYWJsZXMiLA0KICAgICAgICAgICAgICAgIG1hcHBpbmcgPSBhZXMoY29sb3VyPSBmYWN0b3IoU3Vydml2ZWQpKSwNCiAgICAgICAgICAgICAgICBwcm9ncmVzcyA9IEZBTFNFLCANCiAgICAgICAgICAgICAgICBsb3dlcj1saXN0KGNvbWJvPXdyYXAoImZhY2V0aGlzdCIsIGJpbndpZHRoPTAuOCkpKSArDQogICAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSkpICsgDQogICAgICAgIHRoZW1lX2J3KCkgKw0KICAgICAgICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJTZXQxIikgKw0KICAgICAgICBzY2FsZV9jb2xvcl9icmV3ZXIocGFsZXR0ZT0iU2V0MSIpDQpnDQpgYGANCg0Kwr9RdcOpIHB1ZWRlbiBkZWNpciBkZSBsYSByZWxhY2nDs24gZW50cmUgY2xhc2UgKFBjbGFzcykgeSBzdXBlcnZpdmVuY2lhIChTdXJ2aXZlZCk/DQoNCsK/WSBlbnRyZSBlZGFkIChBZ2UpIHkgc3VwZXJ2aXZlbmNpYT8NCg0Kwr9DdcOhbGVzIHBhcmVjZW4gc2VyIGJ1ZW5hcyB2YXJpYWJsZXMgcGFyYSBkaXNjcmltaW5hciBlbnRyZSBxdWllbmVzIHNvYnJldml2aWVyb24geSBxdWllbmVzIG5vPw0KDQojIyMgUHJvYmVtb3MgYWxndW5hcyBzb2x1Y2lvbmVzDQoNCiMjIyBSZWdyZXNpw7NuIGxpbmVhbA0KDQpFbiBlc3RlIGNhc28gZXN0YW1vcyBtb2RlbGFuZG8gbGEgcHJvYmFiaWxpZGFkIGRlIGxhIHNpZ3VpZW50ZSBtYW5lcmE6IA0KDQokUChYKT0gXGJldGFfMCArIFxzdW1cbGltaXRzX3tqPTF9XnAgXGJldGFfaiBYJA0KDQpWZWFtb3MgcXVlIHRhbiBidWVubyBlcyBlbCBtb2RlbG8gbGluZWFsIHBhcmEgZXN0bywgdXNhbmRvIGxhIGVkYWQgY29tbyBwcmVkaWN0b3IuDQoNCmBgYHtyfQ0KbXJsIDwtIGRmdGl0YW5pY190cmFpbiAlPiUgDQogICAgICAgICAgICAgIGxtKGZvcm11bGEgPSBTdXJ2aXZlZCB+IEFnZSkgDQp0ZHkgPSBtcmwgJT4lIHRpZHkoKSANCnRkeQ0KbXJsICU+JSBnbGFuY2UoKQ0KYGBgDQoNCkxvcyBlc3RpbWFkb3JlcyBzb24gc2lnbmlmaWNhdGl2b3MgeSBlbCB0ZXN0IGRlIHNpZ25pZmljYXRpdmlkYWQgZ2xvYmFsIGRlbCBtb2RlbG8gdGFtYmnDqW4gZXMgc2lnbmlmaWNhdGl2by4NCg0KVmVhbW9zIHVuIGdyw6FmaWNvIGRlIG51ZXN0cm8gbW9kZWxvLg0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmdncGxvdChkZnRpdGFuaWNfdHJhaW4sIGFlcyhBZ2UsIFN1cnZpdmVkKSkgKyANCiAgZ2VvbV9wb2ludChhZXMoY29sb3I9ZmFjdG9yKFN1cnZpdmVkKSkpICsNCiAgc2NhbGVfY29sb3JfYnJld2VyKHBhbGV0dGUgPSAiU2V0MSIpICsgDQogIGdlb21fYWJsaW5lKGludGVyY2VwdCA9IHRkeSRlc3RpbWF0ZVsxXSwgc2xvcGUgPSB0ZHkkZXN0aW1hdGVbMl0sIGNvbG9yPSdmb3Jlc3RncmVlbicsIHNpemU9MikgKyANCiAgbGFicyh0aXRsZT0iTW9kZWxvIExpbmVhbCBTaW1wbGUiLCBjb2xvcj0nQ2xhc2UnKSArDQogIGxpbXMoeT1jKC0xLDIpKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNClBhcmVjZSB0ZW5lciBiYXN0YW50ZXMgcHJvYmxlbWFzIHBhcmEgZXN0aW1hciBsYSBwcm9iYWJpbGlkYWQgZGUgc3VwZXJ2aXZlbmNpYSBkZSBsb3MgaW5kaXZpZHVvOiBubyBleGlzdGUgdW4gcHVudG8gZGUgY29ydGUgY2xhcm8sIGxhIHByZWRpY2Npw7NuIHBvZHLDrWEgc2VyIG1heW9yIGEgMSBvIG1lbm9yIGEgY2VybyBsbGVnYWRvIGVsIGNhc28uDQoNCiMjIFJlZ3Jlc2nDs24gTG9nw61zdGljYQ0KDQpQYXJhIGV2aXRhciBlc3RvcyBwcm9ibGVtYXMsIHVzYW1vcyBsYSAqKmZ1bmNpw7NuIGxvZ8Otc3RpY2EqKi4NCg0KJFAoWT0xfFgpPSBcZnJhY3tlXntcYmV0YV8wICsgXHN1bVxsaW1pdHNfe2o9MX1ecCBcYmV0YV9qIFh9fXsxK2Vee1xiZXRhXzAgKyBcc3VtXGxpbWl0c197aj0xfV5wIFxiZXRhX2ogWH19JA0KDQpFbCBsYWRvIGRlcmVjaG8gc2UgbGxhbWEgKipleHBpdCoqDQoNCkVzdGEgZnVuY2nDs24gYWNvdGEgZWwgcmVzdWx0YWRvIGVudHJlIDAgeSAxLCBsbyBjdWFsIGVzIG11Y2hvIG3DoXMgYWRlY3VhZG8gcGFyYSBtb2RlbGFyIHVuYSBwcm9iYWJpbGlkYWQuDQoNCkx1ZWdvIGRlIGhhY2VyIGFsZ3VuYXMgb3BlcmFjaW9uZXMsIHBvZGVtb3MgbGxlZ2FyIGEgbGEgZXhwcmVzacOzbjoNCg0KJFxsb2cge1xmcmFje1AoeCl9ezEtUCh4KX19PSBcYmV0YV8wICsgXHN1bVxsaW1pdHNfe2o9MX1ecCBcYmV0YV9qIFgkDQoNCkVsIGxhZG8gaXpxdWllcmRvIGVzIGVsIGxvZ2FyaXRtbyBkZSBsb3MgKipvZGRzKiogeSBzZSBsbGFtYSAqKmxvZ2l0KiouDQoNCiMjIyBNb2RlbG8gDQoNCkxhIGZ1bmPDrcOzbiBgZ2xtKClgIG5vcyBwZXJtaXRlIGNyZWFyIHVuIG1vZGVsbyBsaW5lYWwgZ2VuZXJhbGl6YWRvIChHZW5lcmFsaXplZCBMaW5lYXIgTW9kZWwpLiBBbCBpZ3VhbCBxdWUgbGEgZnVuY2nDs24gYGxtKClgIHRvbWEgY29tbyBhcmd1bWVudG9zIHVuYSAqKmZvcm11bGEqKiB5IGxvcyAqKmRhdG9zKiogcGVybyB0YW1iacOpbiBzZSBkZWJlIGVzcGVjaWZpY2FyIGVsIGFyZ3VtZW50byAqKmZhbWlseSoqOiBpbmRpY2Ftb3MgbGEgZGlzdHJpYnVjacOzbiBkZWwgZXJyb3IgeSBsYSBmdW5jacOzbiBsaW5rIHF1ZSB2YW1vcyBhIHV0aWxpemFyIGVuIGVsIG1vZGVsby4gDQoNCkFsZ3VuYXMgZmFtaWxpYXMgc29uOg0KDQoqICpCaW5vbWlhbCo6IGxpbms9bG9naXQNCg0KKiAqUG9pc3Nvbio6IGxpbms9bG9nDQoNCiogKkdhdXNzaWFuYSo6IGxpbms9aWRlbnRpZGFkDQoNCkNvbW8gZXN0YW1vcyB0cmFiYWphbmRvIGNvbiB1biBmZW7Ds21lbm8gcXVlIHN1cG9uZW1vcyB0aWVuZSB1bmEgZGlzdHJpYnVjacOzbiBiaW5vbWlhbCwgYXPDrSBsbyBlc3BlY2lmaWNhbW9zIGVuIGVsIHBhcsOhbWV0cm8gKipmYW1pbHkqKi4NCg0KUmVhbGl6YW1vcyB1biBtb2RlbG8gZGUgcmVncmVzacOzbiBsb2fDrXN0aWNhIHBhcmEgcHJlZGVjaXIgbGEgc3VwZXJ2aXZlbmNpYSBlbiBmdW5jacOzbiBkZSAqKlBjbGFzcyoqLCAqKlNleCoqIHkgKipBZ2UqKi4gDQoNCmBgYHtyfQ0KIyBtb2RlbG8gZGUgcmVncmVzacOzbiBsb2fDrXN0aWNhIA0KZ2xtMSA8LSBnbG0oZGF0YSA9IGRmdGl0YW5pY190cmFpbiwgU3Vydml2ZWQgfiBQY2xhc3MgKyBTZXggKyBBZ2UsIGZhbWlseSA9ICdiaW5vbWlhbCcpDQojIHZlbyBsb3MgcmVzdWx0YWRvcw0KdGlkeShnbG0xKQ0KZ2xhbmNlKGdsbTEpDQpgYGANCg0KU2UgdmEgYSBkaXNjdXRpciBzb2JyZSBsYSBpbnRlcnByZXRhY2nDs24gZGUgY29lZmljaWVudGVzIHkgbGEgZXZhbHVhY2nDs24gZW4gbGFzIHNpZ3VpZW50ZXMgc2VjY2lvbmVzLg0KDQojIyMgQ3JlYWNpw7NuIGRlIGbDs3JtdWxhcw0KDQpQYXJhIGNyZWFyIHZhcmlvcyBtb2RvZWxvcyBkZSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgcG9kZW1vcyB1dGlsaXphciBsYSBmdW5jacOzbiBgZm9ybXVsYXNgIGRlbCBwYXF1ZXRlICoqbW9kZWxyKiogcGFyYSBjcmVhciB1biBvYmpldG8gcXVlIGNvbnRpZW5lIHRvZGFzIGxhcyBmw7NybXVsYXMgcXVlIHZhbW9zIGEgdXRpbGl6YXIuIA0KDQpFbiBgLnJlc3BvbnNlYCBlc3BlY2lmaWNhbW9zIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSBkZSBudWVzdHJhcyBmw7NybXVsYXMgeSBsdWVnbyBub21icmFtb3MgbGFzIGbDs3JtdWxhcyBxdWUgcXVlcmFtb3MgYXJtYXIuDQoNCkFzw60sIGFybWFyZW1vcyBkaXN0aW50b3MgbW9kZWxvcyBjb21iaW5hbmRvIGRpc3RpbnRhcyB2YXJpYWJsZXMuIFNlIGdlbmVyYW4gNyBtb2RlbG9zIGRpc3RpbnRvcyBwYXJhIHByZWRlY2lyIGxhIHN1cGVydml2ZW5jaWEgZW4gZnVuY2nDs24gZGUgZGlzdGludGFzIGNvbWJpbmFjaW9uZXMgZGUgbGFzIHZhcmlhYmxlcyBwY2xhc3MsIHNleCwgYWdlIHkgZmFyZS4gDQoNCmBgYHtyfQ0KIyBDcmVhY2nDs24gZGUgZsOzcm11bGFzDQpsb2dpdF9mb3JtdWxhcyA8LSBmb3JtdWxhcygucmVzcG9uc2UgPSB+IFN1cnZpdmVkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3MgPSB+IFBjbGFzcywgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzZXggPSB+IFNleCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhZ2UgPSB+IEFnZSwgIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgUGNTID0gfiBQY2xhc3MgKyBTZXgsICMgbW9kZWxvIGNvbiBsYXMgdmFyaWFibGVzIHF1ZSBtw6FzIHBhcmVjZW4gZGl2aWRpciBlbCB0YXJnZXQNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFBjU0EgPSB+IFBjbGFzcyArIFNleCArIEFnZSwgIyBtb2RlbG8gZ2xtMQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgUGNTRiA9IH4gUGNsYXNzICsgU2V4ICsgRmFyZSwgICMgbW9kZWxvIHByZXZpbyBzaW4gQWdlDQogICAgICAgICAgICAgICAgICAgICAgICAgICBQY1NBRiA9IH4gUGNsYXNzICsgU2V4ICsgQWdlICsgRmFyZSAjIG1vZGVsbyBnbG0xIGNvbiBGYXJlICANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICkNCmxvZ2l0X2Zvcm11bGFzICMgb2JzZXJ2YW1vcyBlbCBvYmpldG8gZm9ybXVsYXMNCmBgYA0KDQojIyMgQ3JlYWNpw7NuIGRlIG1vZGVsb3MNCg0KUHJvY2VkZW1vcyBhIGNyZWFyIGxvcyBtb2RlbG9zIGEgcGFydGlyIGRlIGVzdGFzIGbDs3JtdWxhcy4NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQptb2RlbHMgPC0gZGF0YV9mcmFtZShsb2dpdF9mb3JtdWxhcykgJT4lICMgZGF0YWZyYW1lIGEgcGFydGlyIGRlbCBvYmpldG8gZm9ybXVsYXMNCiAgbXV0YXRlKG1vZGVscyA9IG5hbWVzKGxvZ2l0X2Zvcm11bGFzKSwgIyBjb2x1bW5hIGNvbiBsb3Mgbm9tYnJlcyBkZSBsYXMgZm9ybXVsYXMNCiAgICAgICAgIGV4cHJlc3Npb24gPSBwYXN0ZShsb2dpdF9mb3JtdWxhcyksICMgY29sdW1uYSBjb24gbGFzIGV4cHJlc2lvbmVzIGRlIGxhcyBmb3JtdWxhcw0KICAgICAgICAgbW9kID0gbWFwKGxvZ2l0X2Zvcm11bGFzLCB+Z2xtKC4sIGZhbWlseSA9ICdiaW5vbWlhbCcsIGRhdGEgPSBkZnRpdGFuaWNfdHJhaW4pKSkgIyBRdWUgZXN0YW1vcyBoYWNpZW5kbyBhY8OhPyBRdWUgdmFtb3MgYSBlbmNvbnRyYXIgZW4gbGEgY29sdW1uYT8NCm1vZGVscw0KYGBgDQoNCiMjIyBNb2RlbG9zIHNpbXBsZXMNCg0KUHJvYmFtb3MgbG9zIHByaW1lcm9zIHRyZXMgbW9kZWxvcywgYXF1ZWxsb3MgcXVlIHRpZW5lbiB1biDDum5pY28gcHJlZGljdG9yLiBVc2Ftb3MgbGEgZnVuY2nDs24gX3RpZHlfIHBhcmEgb2J0ZW5lciBsb3MgcGFyw6FtZXRyb3MgZXN0aW1hZG9zIHBhcmEgZXN0b3MgdHJlcyBtb2RlbG9zLg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCm1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHMgJWluJSBjKCdjbGFzcycsJ3NleCcsJ2FnZScpKSAlPiUNCiAgbXV0YXRlKHRpZHkgPSBtYXAobW9kLCB0aWR5KSkgICMgUXXDqSByZWFsaXphbW9zIGVuIGVzdGUgcGFzbz8gUXVlIHZhIGEgdGVuZXIgZXN0YSBjb2x1bW5hPw0KYGBgDQoNClBhcmEgYWNjZWRlciBhIGxvcyBlbGVtZW50b3MgZGUgbGEgbnVldmEgY29sdW1uYSBfdGlkeV8gZGViZW1vcyBkZXNhbmlkYXJsYSAodXNhbmRvIGB1bm5lc3QoKWApLiBFbCBhbmlkYWRvIGNyZWEgdW5hIGNvbHVtbmEgZGUgbGlzdGFzIGRlIGRhdGFmcmFtZXMsIGVzIGltcGzDrWNpdGFtZW50ZSB1bmEgb3BlcmFjacOzbiBkZSByZXN1bWVuOiBvYnRpZW5lIHVuYSBmaWxhIHBhcmEgY2FkYSBncnVwbyBkZWZpbmlkbyBwb3IgbGFzIGNvbHVtbmFzIG5vIGFuaWRhZGFzLiBEZXNhbmlkYXIgbG8gYXBsYW5hIGRlIG51ZXZvIGVuIGNvbHVtbmFzIHJlZ3VsYXJlcy4gUGFyYSBtYXlvciBkZXRhbGxlLCBzdWdlcmltb3MgdmVyIGxhIGRvY3VtZW50YWNpw7NuIGRlIHRpZHlyIHNvYnJlIFtOZXN0IGFuZCB1bm5lc3RdKGh0dHBzOi8vdGlkeXIudGlkeXZlcnNlLm9yZy9yZWZlcmVuY2UvbmVzdC5odG1sKS4gDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KbW9kZWxzICU+JSANCiAgZmlsdGVyKG1vZGVscyAlaW4lIGMoJ2NsYXNzJywnc2V4JywnYWdlJykpICU+JQ0KICBtdXRhdGUodGlkeSA9IG1hcChtb2QsIHRpZHkpKSAlPiUgICMgUXXDqSByZWFsaXphbW9zIGVuIGVzdGUgcGFzbz8gUXVlIHZhIGEgdGVuZXIgZXN0YSBjb2x1bW5hPw0KICB1bm5lc3QodGlkeSkgJT4lIA0KICBtdXRhdGUoZXN0aW1hdGU9cm91bmQoZXN0aW1hdGUsNSksICMgcmVkb25kZWFtb3MgdmFsb3JlcyBwYXJhIGZhY2lsaXRhciBsZWN0dXJhDQogICAgICAgICBwLnZhbHVlPXJvdW5kKHAudmFsdWUsNCkpDQpgYGANCg0KT2JzZXJ2YW1vcyBxdWUgdG9kb3MgbG9zIG1vZGVsb3MgdGllbmVuIGNvZWZpY2llbnRlcyBzaWduaWZpY2F0aXZvcyBhdW5xdWUgZWwgZGUgQWdlIHNlIGVuY3VlbnRyYSBtdXkgY2VyY2FubyBhIG51ZXN0cm8gdmFsb3IgZGUgcmVjaGF6by4NCg0KIyMgSW50ZXJwcmV0YWNpw7NuIGRlIGxvcyBjb2VmaWNpZW50ZXMNCg0KUmVjb3JkYW5kbyBsYSBlY3VhY2nDs24gcGFyYSBtb2RlbGFyIGxhIHByb2JhYmlsaWRhZDoNCg0KJFAoWT0xfFgpPSBcZnJhY3tlXntcYmV0YV8wICsgXHN1bVxsaW1pdHNfe2o9MX1ecCBcYmV0YV9qIFh9fXsxK2Vee1xiZXRhXzAgKyBcc3VtXGxpbWl0c197aj0xfV5wIFxiZXRhX2ogWH19JA0KDQpTZSBvYnNlcnZhIHF1ZSBhaG9yYSBsYXMgdmFyaWFibGVzIHlhIG5vIHRpZW5lbiB1bmEgcmVsYWNpw7NuIGxpbmVhbCBjb24gbGEgcHJvYmFiaWxpZGFkLiBFbiBlc3RlIG1vZGVsbyB1biBjb2VmaWNpZW50ZSBwb3NpdGl2byBpbmRpY2EgcXVlIGZyZW50ZSBhIGF1bWVudG9zIGRlIGRpY2hhIHZhcmlhYmxlIGxhIHByb2JhYmlsaWRhZCBhdW1lbnRhLCBtaWVudHJhcyBxdWUgdW4gY29lZmljaWVudGUgbmVnYXRpdm8gbm9zIGluZGljYSBsbyBjb250cmFyaW8uIFBhcmEgbnVlc3Ryb3MgbW9kZWxvczoNCg0KKipNb2RlbG8gY2xhc3MqKg0KDQoqIM6yMCA9IGByIHJvdW5kKChtb2RlbHMgJT4lIGZpbHRlcihtb2RlbHMgJWluJSBjKCdjbGFzcycpKSAlPiUgbXV0YXRlKHRpZHkgPSBtYXAobW9kLCB0aWR5KSkgJT4lICB1bm5lc3QodGlkeSkgJT4lIGZpbHRlcih0ZXJtID09ICcoSW50ZXJjZXB0KScpICU+JSBzZWxlY3QoZXN0aW1hdGUpKSRlc3RpbWF0ZSw1KWAgY29ycmVzcG9uZGUgYSBwZXJzb25hcyBxdWUgdmlhamFyb24gZW4gcHJpbWVyYSBjbGFzZS4NCg0KKiDOsjEgPSBgciByb3VuZCgobW9kZWxzICU+JSBmaWx0ZXIobW9kZWxzICVpbiUgYygnY2xhc3MnKSkgJT4lIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCwgdGlkeSkpICU+JSAgdW5uZXN0KHRpZHkpICU+JSBmaWx0ZXIodGVybSA9PSAnUGNsYXNzMicpICU+JSBzZWxlY3QoZXN0aW1hdGUpKSRlc3RpbWF0ZSw1KWAgeSDOsjIgPSBgciByb3VuZCgobW9kZWxzICU+JSBmaWx0ZXIobW9kZWxzICVpbiUgYygnY2xhc3MnKSkgJT4lIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCwgdGlkeSkpICU+JSAgdW5uZXN0KHRpZHkpICU+JSBmaWx0ZXIodGVybSA9PSAnUGNsYXNzMycpICU+JSBzZWxlY3QoZXN0aW1hdGUpKSRlc3RpbWF0ZSw1KWAgY29ycmVzcG9uZGVuIGEgcGVyc29uYXMgcXVlIHZpYWphcm9uIGVuIGxhIHNlZ3VuZGEgeSB0ZXJjZXJhIGNsYXNlLCByZXNwZWN0aXZhbWVudGUuIEVsIGNvZWZpY2llbnRlIGVzdGltYWRvIGVuIGFtYm9zIGNhc29zIGZ1ZSBuZWdhdGl2bywgaW5kaWNhbmRvIHF1ZSBsYSAqKnByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmVuY2lhIGRpc21pbnV5ZSoqIGVuIGNvbXBhcmFjacOzbiBjb24gbGEgcHJpbWVyYSBjbGFzZS4gIEFsIHNlciBtw6FzIG5lZ2F0aXZvIGVsIGNvZWZpY2llbnRlIGVzdGltYWRvIGRlIFBjbGFzczMsIHNlIGVzcGVyYSBxdWUgbGEgcHJvYmFiaWxpZGFkIGRlIFN1cGVydml2ZW5jaWEgc2VhIG1lbm9yIGHDum4gcGFyYSBlc3RhIGNsYXNlIHF1ZSBwYXJhIHVuIHBhc2FqZXJvIGVuIGNsYXNlIDIuDQoNCioqTW9kZWxvIHNleCoqDQoNCiogzrIwID0gYHIgcm91bmQoKG1vZGVscyAlPiUgZmlsdGVyKG1vZGVscyAlaW4lIGMoJ3NleCcpKSAlPiUgbXV0YXRlKHRpZHkgPSBtYXAobW9kLCB0aWR5KSkgJT4lICB1bm5lc3QodGlkeSkgJT4lIGZpbHRlcih0ZXJtID09ICcoSW50ZXJjZXB0KScpICU+JSBzZWxlY3QoZXN0aW1hdGUpKSRlc3RpbWF0ZSw1KWAgY29ycmVzcG9uZGUgYSBtdWplcmVzIHF1ZSB2aWFqYXJvbiBhIGJvcmRvIGRlbCB0aXRhbmljLg0KDQoqIM6yMSA9IGByIHJvdW5kKChtb2RlbHMgJT4lIGZpbHRlcihtb2RlbHMgJWluJSBjKCdzZXgnKSkgJT4lIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCwgdGlkeSkpICU+JSAgdW5uZXN0KHRpZHkpICU+JSBmaWx0ZXIodGVybSA9PSAnU2V4bWFsZScpICU+JSBzZWxlY3QoZXN0aW1hdGUpKSRlc3RpbWF0ZSw1KWAgcmVwcmVzZW50YSBhIGxvcyBob21icmVzIHF1ZSB2aWFqYXJvbiBlbiBlbCB0aXRhbmljIGUgaW5kaWNhIHF1ZSAqKmxhIHByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmVuY2lhIHNlIHJlZHVjZSoqIGVuIGNvbXBhcmFjacOzbiBhIGxhcyBtdWplcmVzLg0KDQoqKk1vZGVsbyBhZ2UqKg0KDQoqIM6yMSA9IGByIHJvdW5kKChtb2RlbHMgJT4lIGZpbHRlcihtb2RlbHMgJWluJSBjKCdhZ2UnKSkgJT4lIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCwgdGlkeSkpICU+JSAgdW5uZXN0KHRpZHkpICU+JSBmaWx0ZXIodGVybSA9PSAnQWdlJykgJT4lIHNlbGVjdChlc3RpbWF0ZSkpJGVzdGltYXRlLDUpYCBpbmRpY2EgcXVlIGxhICoqcHJvYmFiaWxpZGFkIGRlIHN1cGVydml2ZW5jaWEgZGlzbWludXllKiogcG9yIGNhZGEgYcOxbyBtw6FzIGRlIGVkYWQgZGUgbGEgcGVyc29uYS4NCg0KIyMjIMK/UXXDqSBwYXNhcsOtYSBlbiB1biBtb2RlbG8gbcO6bHRpcGxlPw0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCm1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHMgJWluJSBjKCdQY1NBJykpICU+JQ0KICBtdXRhdGUodGlkeSA9IG1hcChtb2QsIHRpZHkpKSAlPiUgDQogIHVubmVzdCh0aWR5KSAlPiUgDQogIG11dGF0ZShlc3RpbWF0ZT1yb3VuZChlc3RpbWF0ZSw1KSwNCiAgICAgICAgIHAudmFsdWU9cm91bmQocC52YWx1ZSw0KSkNCmBgYA0KQXPDrSBjb21vIHNlIG9ic2VydsOzIGVuIGVsIG1vZGVsbyBzaW1wbGUgcGFyYSBsYSB2YXJpYWJsZSBQY2xhc3MsIGVuIGVzdGUgY2FzbyBsb3MgY29lZmljaWVudGVzIGVzdGltYWRvcyBkZSBQY2xhc3MyIHkgUGNsYXNzMyByZXN1bHRhbiBuZWdhdGl2b3MsIGluZGljYW5kbyBxdWUgc2kgZWwgcGFzYWplcm8gcGVydGVuZWNlIGEgc2VndW5kYSBvIHRlcmNlcmEgY2xhc2UsIHN1IHByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmllbmNpYSBlc3BlcmFkYSBkaXNtaW51eWUgcmVzcGVjdG8gZGUgdW5hIHBlcnNvbmEgZGUgcHJpbWVyYSBjbGFzZSwgZGFkYXMgbGFzIGRlbcOhcyB2YXJpYWJsZXMgZGVsIG1vZGVsbyAoU2V4IHkgQWdlKS4gDQoNClBhcmEgbGEgdmFyaWFibGUgU2V4IG9ic2VydmFtb3MgcXVlIGVsIGNvZWZpY2llbnRlIFNleE1hbGUgcmVzdWx0YSBuZWdhdGl2bzsgZXN0byBzaWduaWZpY2EgcXVlIGxhIHByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmVuY2lhIGVzcGVyYWRhIGRlIGxvcyBob21icmVzIGRpc21pbnV5ZSByZXNwZWN0byBhIGxhIGRlIGxhcyBtdWplcmVzLCBkYWRhcyBsYXMgZGVtw6FzIHZhcmlhYmxlcyBkZWwgbW9kZWxvLg0KDQpQYXJhIGxhIHZhcmlhYmxlIEFnZSwgZWwgY29lZmljaWVudGUgZXN0aW1hZG8gdGFtYmnDqW4gcmVzdWx0YSBuZWdhdGl2bywgaW5kaWNhbmRvIHF1ZSBhbnRlIHVuIGF1bWVudG8gZW4gbGEgZWRhZCBkZWwgcGFzYWplcm8sIHN1IHByb2JhYmlsaWRhZCBkZSBzdXBlcnZpdmVuY2lhIGVzcGVyYWRhIGRpc21pbnV5ZSwgZGFkYXMgbGFzIGRlbcOhcyB2YXJpYWJsZXMuDQoNCkFzaW1pc21vLCBzZSBvYnNlcnZhIHF1ZSBsb3MgY29lZmljaWVudGVzIHJlc3VsdGFuIHRvZG9zIHNpZ25pZmljYXRpdm9zIGVuIGVzdGUgbW9kZWxvIChwLXZhbG9yPDAuMDUpLiANCg0KUmVjb21lbmRhbW9zIGxlZXIgZWwgW2NhcMOtdHVsbyBkZSByZWdyZXNpw7NuIGxvZ8Otc3RpY2FdKGh0dHBzOi8vY2hyaXN0b3BobS5naXRodWIuaW8vaW50ZXJwcmV0YWJsZS1tbC1ib29rL2xvZ2lzdGljLmh0bWwpIGRlICpJbnRlcnByZXRhYmxlIE1hY2hpbmUgTGVhcm5pbmc6IEEgR3VpZGUgZm9yIE1ha2luZyBCbGFjayBCb3ggTW9kZWxzIEV4cGxhaW5hYmxlKiBkZSBNb2xuYXIgQ3Jpc3RvcGggcGFyYSB1bmEgZGlzY3VzacOzbiBtw6FzIHByb2Z1bmRhIGRlIGxhIGludGVycHJldGFjacOzbiBkZSB1biBtb2RlbG8gZGUgcmVncmVzacOzbiBsb2fDrXN0aWNhLg0KDQojIyBFdmFsdWFjacOzbiBkZSB0b2RvcyBsb3MgbW9kZWxvcw0KDQpDb24gYG1hcCgpYCBhZ3JlZ2Ftb3MgbGEgZnVuY2nDs24gYGdsYW5jZWAgcGFyYSB0cmFlciBpbmZvcm1hY2nDs24gcmVsZXZhbnRlIHBhcmEgbGEgZXZhbHVhY2nDs24gZGVsIG1vZGVsby4gQ29uIGB1bm5lc3QoKWAgYWNjZWRlbW9zIGEgZGljaGEgaW5mb3JtYWNpw7NuLiBQb3Igw7psdGltbywgYWdyZWdhbW9zIHVuYSBjb2x1bW5hIGNvbiBlbCBwb3JjZW50YWplIGRlIGRldmlhbmNlIGV4cGxpY2FkbyBwb3IgY2FkYSBtb2RlbG8geSBvcmRlbmFtb3MgZWwgZGF0YXNldCBzZWfDum4gc3UgdmFsb3IgZGUgZGV2aWFuY2UuDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KIyBDYWxjdWxhciBsYXMgbWVkaWRhcyBkZSBldmFsdWFjacOzbiBwYXJhIGNhZGEgbW9kZWxvDQptb2RlbHMgPC0gbW9kZWxzICU+JSANCiAgbXV0YXRlKGdsYW5jZSA9IG1hcChtb2QsZ2xhbmNlKSkNCiMgT2J0ZW5lciBsYXMgbWVkaWRhcyBkZSBldmFsdWFjaW9uIGRlIGludGVyZXMNCm1vZGVscyAlPiUgDQogIHVubmVzdChnbGFuY2UpICU+JQ0KICAjIENhbGN1bGFtb3MgbGEgZGV2aWFuY2UgZXhwbGljYWRhDQogIG11dGF0ZShwZXJjX2V4cGxhaW5lZF9kZXYgPSAxLWRldmlhbmNlL251bGwuZGV2aWFuY2UpICU+JSANCiAgc2VsZWN0KC1jKG1vZGVscywgZGYubnVsbCwgQUlDLCBCSUMpKSAlPiUgDQogIGFycmFuZ2UoZGV2aWFuY2UpDQpgYGANCg0KTG9zIG1vZGVsb3MgcXVlIGluY2x1eWVuIGxhcyAzIHZhcmlhYmxlcyBQY2xhc3MsIFNleCB5IEFnZSBwYXJlY2VuIHNlciBsb3MgcXVlIG1pbmltaXphbiBsYSBkZXZpYW5jZS4gT2JzZXJ2YW1vcyB0YW1iacOpbiBxdWUgbG9zIDIgw7psdGltb3MgbW9kZWxvcyByZWR1Y2VuIG11eSBwb2NvIGxhIGRldmlhbmNlIHJlc3BlY3RvIGEgbGEgZGV2aWFuY2UgbnVsYS4NCg0KIyMjIEdyw6FmaWNvcyBkZSBFdmFsdWFjacOzbg0KDQpSZWFsaXphbW9zIGxvcyBncsOhZmljb3MgcGFyYSBlbCBtb2RlbG8gY29tcGxldG8geSB1bm8gZGUgbG9zIG1vZGVsb3MgY29uIG1heW9yIGRldmlhbmNlIChBZ2UpLg0KDQpDb21lbnphbW9zIGFncmVnYW5kbyBsYXMgcHJlZGljY2lvbmVzIGNvbiBgYXVnbWVudGAgY29uIGVsIHBhcsOhbWV0cm8gYHR5cGU9InJlc3BvbnNlImAuIExhIGZ1bmNpw7NuIGF1Z21lbnQgaGVyZWRhIGVsIGFyZ3VtZW50byB0eXBlLnByZWRpY3QgZGUgbGEgZnVuY2nDs24gcHJlZGljdC4NCg0KICAqIFNpIGB0eXBlLnByZWRpY3QgPSAnbGluaydgIGxhIHByZWRpY2Npw7NuIGVzIGVuIHTDqXJtaW5vcyBkZSBsYSBmdW5jacOzbiBsaW5rLiBFbiBudWVzdHJvIGNhc28gc29uIGVsIGxvZ2FyaXRtbyBkZSBsYXMgb2RkcywgZXMgZGVjaXIsIGxvcyB2YWxvcmVzIHF1ZSB0b21hIGxhIGV4cHJlc2nDs24gbG9naXQuDQogIA0KICAqIFNpIGB0eXBlLnByZWRpY3QgPSAncmVzcG9uc2UnYCBsYSBwcmVkaWNjacOzbiBzb24gbGFzIHByb2JhYmlsaWRhZGVzIGRlIHF1ZSBsYSBvYnNlcnZhY2nDs24gcGVydGVuZXpjYSBhIGxhIGNsYXNlIHBvc2l0aXZhLiBFbiBudWVzdHJvIGNhc28sIGRldnVlbHZlIGxhIHByb2JhYmlsaWRhZCBkZSBsYSBxdWUgcGVyc29uYSBzb2JyZXZpdmEuDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KIyBBw7FhZGlyIGxhcyBwcmVkaWNjaW9uZXMNCm1vZGVscyA8LSBtb2RlbHMgJT4lIA0KICBtdXRhdGUocHJlZD0gbWFwKG1vZCwgYXVnbWVudCwgdHlwZS5wcmVkaWN0ID0gInJlc3BvbnNlIikpDQojT2JzZXJ2YWNpb25lcyBjb24gcHJvYmFiaWxpZGFkIG3DoXMgYmFqYQ0KbW9kZWxzJHByZWQkUGNTQUYgJT4lIGFycmFuZ2UoLmZpdHRlZCkgJT4lIGhlYWQoMTApDQojT2JzZXJ2YWNpb25lcyBjb24gcHJvYmFiaWxpZGFkIG3DoXMgYWx0YQ0KbW9kZWxzJHByZWQkUGNTQUYgJT4lIGFycmFuZ2UoZGVzYyguZml0dGVkKSkgJT4lIGhlYWQoMTApDQpgYGANCg0KR3VhcmRhbW9zIGxhcyBwcmVkaWNjaW9uZXMgcGFyYSBsb3MgbW9kZWxvcyBtZW5jaW9uYWRvcy4NCg0KYGBge3J9DQojIE1vZGVsbyBjb21wbGV0bw0KcHJlZGljdGlvbl9mdWxsIDwtIG1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHM9PSJQY1NBRiIpICU+JSANCiAgdW5uZXN0KHByZWQpDQojTW9kZWxvIG1hbG8NCnByZWRpY3Rpb25fYmFkIDwtIG1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHM9PSJhZ2UiKSAlPiUgDQogIHVubmVzdChwcmVkKQ0KYGBgDQoNCg0KIyMjIyBWaW9saW4gcGxvdHMNCg0KYGBge3J9DQojIGdyYWZpY2Ftb3MgZWwgbW9kZWxvIGNvbXBsZXRvDQp2aW9saW5fZnVsbCA9IGdncGxvdChwcmVkaWN0aW9uX2Z1bGwsIGFlcyh4PVN1cnZpdmVkLCB5PS5maXR0ZWQsIGdyb3VwPVN1cnZpdmVkLCBmaWxsPWZhY3RvcihTdXJ2aXZlZCkpKSArIA0KICBnZW9tX3Zpb2xpbigpICsNCiAgdGhlbWVfYncoKSArDQogIGd1aWRlcyhzY2FsZT0ibm9uZSIpICsNCiAgbGFicyh0aXRsZT0nVmlvbGluIHBsb3QnLCBzdWJ0aXRsZT0nTW9kZWxvIGNvbXBsZXRvJywgeT0nUHJlZGljdGVkIHByb2JhYmlsaXR5JykNCiMgZ3JhZmljYW1vcyBlbCBtb2RlbG8gbWFsbw0KdmlvbGluX2JhZCA9IGdncGxvdChwcmVkaWN0aW9uX2JhZCwgYWVzKHg9U3Vydml2ZWQsIHk9LmZpdHRlZCwgZ3JvdXA9U3Vydml2ZWQsIGZpbGw9ZmFjdG9yKFN1cnZpdmVkKSkpICsgDQogIGdlb21fdmlvbGluKCkgKyANCiAgdGhlbWVfYncoKSArDQogIGd1aWRlcyhzY2FsZT0ibm9uZSIpICsNCiAgbGFicyh0aXRsZT0nVmlvbGluIHBsb3QnLCBzdWJ0aXRsZT0nTW9kZWxvIG1hbG8nLCB5PSdQcmVkaWN0ZWQgcHJvYmFiaWxpdHknKQ0KIyBtb3N0cmFtb3MgYW1ib3MNCnBsb3RfZ3JpZCh2aW9saW5fYmFkLCB2aW9saW5fZnVsbCkNCmBgYA0KDQpFbiBsb3MgZ3LDoWZpY29zIGRlIHZpb2xpbiBvYnNlcnZhbW9zOg0KDQogICogRW4gZWwgZWplIGRlIGFic2Npc2FzIGxhIGNsYXNlIHZlcmRhZGVyYTogU3Vydml2ZWQgbyBObyBTdXJ2aXZlZCAoMSBvIDApLg0KICANCiAgKiBFbiBlbCBlamUgZGUgb3JkZW5hZGFzIGxhIHByb2JhYmlsaWRhZCBwcmVkaWNoYSBwb3IgbnVlc3RybyBtb2RlbG8uDQogIA0KICAqIEVsIGdyw6FmaWNvIG5vcyBtdWVzdHJhIGxhIGRpc3RyaWJ1Y2nDs24gZGUgbGEgY2FudGlkYWQgZGUgb2JzZXJ2YWNpb25lcyBwb3Igc3UgY2xhc2UgcmVhbCB5IGxhIHByb2JhYmlsaWRhZCBxdWUgbGUgYXNpZ25hIG51ZXN0cm8gbW9kZWxvLg0KDQrCv0N1w6FsIHBhcmVjZSBzZXIgdW4gcHVudG8gZGUgY29ydGUgYWRlY3VhZG8gcGFyYSBjYWRhIG1vZGVsbz8NCg0KIyMjIyBHcsOhZmljbyBkZSBIb3NtZXItTGVtZXNob3cNCg0KU2UgZ2VuZXJhIHVuYSBmdW5jacOzbiBwYXJhIHJlYWxpemFyIHVuIGdyw6FmaWNvIGRlIEhvc21lci1MZW1lc2hvdyBwYXJhIHVuIGRhdGFzZXQuIFBhcmEgZWxsbyBzZSBmaWphbiBsb3Mgc2lndWllbnRlcyBwYXLDoW1ldHJvczogDQogIA0KKiBkYXRhc2V0OiBjb25qdW50byBkZSBkYXRvcw0KDQoqIHByZWRpY3RlZF9jb2x1bW46IGNvbHVtbmEgY29uIGxhIHByb2JhYmlsaWRhZCBwcmVkaWNoYQ0KDQoqIGNsYXNzX2NvbHVtbjogY29sdW1uYSBjb24gbGEgY2xhc2UgYSBwcmVkZWNpcg0KDQoqIHBvc3NpdGl2ZV92YWx1ZTogdmFsb3IgZGUgbGEgY2xhc2UgYSBwcmVkZWNpcg0KDQoqIGJpbnM6IGNhbnRpZGFkIGRlIGdydXBvcyBkZWwgZ3LDoWZpY28NCg0KKiBjb2xvcjogY29sb3IgZGUgbG9zIHB1bnRvcw0KDQoqIG51ZGdlX3g6IGRlc3BsYXphbWllbnRvIGRlIGxhIGV0aXF1ZXRhIGVuIGVsIGVqZSB4DQoNCiogbnVkZ2VfeTogZGVzcGxhemFtaWVudG8gZGUgbGEgZXRpcXVldGEgZW4gZWwgZWplIHkNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpIb3NtZXJfTGVtZXNob3dfcGxvdCA8LSBmdW5jdGlvbihkYXRhc2V0LCBwcmVkaWN0ZWRfY29sdW1uLCBjbGFzc19jb2x1bW4sIGJpbnMsIHBvc2l0aXZlX3ZhbHVlLCBjb2xvcj0nZm9yZXN0Z3JlZW4nLCBudWRnZV94PTAsIG51ZGdlX3k9MC4wNSl7DQogICMgQXNpZ25hciBsb3MgZ3J1cG9zIGEgbGFzIG9ic2VydmFjaW9uZXMgZGUgYWN1ZXJkbyBhIGxhIHByb2JhYmlsaWRhZCBwcmVkaWNoYQ0KICBkYXRhc2V0Wydncm91cCddIDwtIGJpbihkYXRhc2V0W3ByZWRpY3RlZF9jb2x1bW5dLCBuYmlucyA9IGJpbnMsIG1ldGhvZCA9ICdsJywgbGFiZWxzPWMoMTpiaW5zKSkNCiAgIyBDb250YXIgbGEgY2FudGlkYWQgZGUgY2Fzb3MgcG9zaXRpdm9zIHBvciBncnVwbw0KICBwb3NpdGl2ZV9jbGFzcyA8LSBkYXRhc2V0ICU+JSBmaWx0ZXIoISFzeW0oY2xhc3NfY29sdW1uKT09cG9zaXRpdmVfdmFsdWUpICU+JSBncm91cF9ieShncm91cCkgJT4lIGNvdW50KCkNCiAgIyBPYnRlbmVyIGxhIG1lZGlhIGRlIGxhcyBwcmVkaWNjaW9uZXMgcG9yIGdydXBvDQogIEhMX2RmIDwtIGRhdGFzZXQgJT4lIGdyb3VwX2J5KGdyb3VwKSAlPiUgc3VtbWFyaXNlKHByZWQ9bWVhbighIXN5bShwcmVkaWN0ZWRfY29sdW1uKSksIGNvdW50PW4oKSkgJT4lDQogICAgICAgICAgICBpbm5lcl9qb2luKC4scG9zaXRpdmVfY2xhc3MpICU+JQ0KICAgICAgICAgICAgbXV0YXRlKGZyZXE9bi9jb3VudCkNCiAgIyBHcsOhZmljbyANCiAgSE1fcGxvdCA8LSBnZ3Bsb3QoSExfZGYsIGFlcyh4PXByZWQsIHk9ZnJlcSkpICsgDQogICAgZ2VvbV9wb2ludChhZXMoc2l6ZT1uKSwgY29sb3I9Y29sb3IpICsNCiAgICBnZW9tX3RleHQoYWVzKGxhYmVsPW4pLG51ZGdlX3kgPSBudWRnZV95KSsNCiAgICBnZW9tX2FibGluZShzbG9wZSA9IDEsIGludGVyY2VwdCA9IDAsIGxpbmV0eXBlPSdkYXNoZWQnKSArIA0KICAgIHRoZW1lX2J3KCkgKw0KICAgIGxhYnModGl0bGU9J0hvc21lci1MZW1lc2hvdycsIHNpemU9J0Nhc29zJywgeD0iUHJvYmFiaWxpZGFkIFByZWRpY2hhIiwgeT0iRnJlY3VlbmNpYSBvYnNlcnZhZGEiKQ0KICByZXR1cm4oSE1fcGxvdCkNCn0NCmBgYA0KDQpHZW5lcmFtb3MgbG9zIGdyw6FmaWNvcyBwYXNhbmRvbGUgbG8gcGFyw6FtZXRyb3MuIA0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgbW9kZWxvIGNvbXBsZXRvDQpIb3NtZXJfTGVtZXNob3dfcGxvdChwcmVkaWN0aW9uX2Z1bGwsICcuZml0dGVkJywgJ1N1cnZpdmVkJywgMTAsIDEpICsNCiAgbGFicyhzdWJ0aXRsZT0iTW9kZWxvIGNvbXBsZXRvIikNCiMgbW9kZWxvIG1hbG8NCkhvc21lcl9MZW1lc2hvd19wbG90KHByZWRpY3Rpb25fYmFkLCAnLmZpdHRlZCcsICdTdXJ2aXZlZCcsIDEwLCAxLCBjb2xvciA9ICJmaXJlYnJpY2siKSArIGxhYnMoc3VidGl0bGU9Ik1vZGVsbyBtYWxvIikNCg0KYGBgDQoNCkVuIGxvcyAqKmdyw6FmaWNvcyBkZSBIb3NtZXItTGVtZXNob3cqKiBvYnNlcnZhbW9zOg0KDQogICogRW4gZWwgZWplIGRlIGFic2Npc2FzIGxhIHByb2JhYmlsaWRhZCBwcmVkaWNoYSBkZSBzdXBlcnZpdmVuY2lhLg0KICANCiAgKiBFbiBlbCBlamUgZGUgb3JkZW5hZGFzIGxhIGZyZWN1ZW5jaWEgZGUgY2xhc2UsIGVsIGNvY2llbnRlIGVudHJlIGNhbnRpZGFkIGRlIGluZGl2aWR1b3MgU3Vydml2ZWQgeSBlbCB0b3RhbCBkZSBpbmRpdmlkdW9zLg0KICANCiAgKiBMYSBsw61uZWEgcHVudGVhZGEgZGVzaWduYSBsYSBpZ3VhbGRhZCBlbnRyZSBwcm9iYWJpbGlkYWQgcHJlZGljaGEgeSBmcmVjdWVuY2lhIGRlIGNsYXNlLg0KICANCiAgKiBMb3MgY8OtcmN1bG9zLCBxdWUgc2UgY29uc3RydXllbiBkZSBsYSBzaWd1aWVudGUgbWFuZXJhOg0KICAgICAgKiBTZSBkaXZpZGVuIGEgbGFzIG9ic2VydmFjaW9uZXMgZW4gYmlucyBlbiBiYXNlIGEgbGEgcHJvYmFiaWxpZGFkIHByZWRpY2hhDQogICAgICAqIFNlIGNhbGN1bGEgbGEgZnJlY3VlbmNpYSBkZSBjbGFzZSBwYXJhIGNhZGEgYmluDQogICAgICAqIEVuIGJhc2UgYSBlc3RhcyBkb3MgY29vcmRlbmFkYXMgc2UgdWJpY2EgYWwgY8OtcmN1bG8gZW4gZWwgZ3LDoWZpY28NCiAgICAgICogRWwgbsO6bWVybyB5IHRhbWHDsW8gaW5kaWNhbiBsYSBjYW50aWRhZCBkZSBvYnNlcnZhY2lvbmVzIGVuIGRpY2hvIGdydXBvDQoNCkFxdWVsbG9zICoqY8OtcmN1bG9zIHF1ZSBzZSB1YmlxdWVuIHBvciBlbmNpbWEqKiBkZSBsYSBsw61uZWEgcHVudGVhZGEgaW5kaWNhbiBxdWUgZWwgKiptb2RlbG8gZXN0w6Egc3ViZXN0aW1hbmRvKiogbGEgcHJvYmFiaWxpZGFkIHBhcmEgZGljaG9zIGdydXBvcy4gTWllbnRyYXMgcXVlIHNpIGxvcyAqKmPDrXJjdWxvcyBzZSB1YmljYW4gcG9yIGRlYmFqbyoqIGVsIG1vZGVsbyBlc3TDoSAqKnNvYnJlZXN0aW1hbmRvKiogbGEgcHJvYmFiaWxpZGFkIHBhcmEgZGljaG9zIGdydXBvcy4NCg0Kwr9QYXJhIHF1w6kgdmFsb3JlcyBwYXJlY2UgZXhpc3RpciB1bmEgc29icmVlc3RpbWFjacOzbiBkZSBsYSBwcm9iYWJpbGlkYWQ/IMK/UGFyYSBjdcOhbGVzIHN1YmVzdGltYWNpw7NuPw0KDQojIyMjIEN1cnZhcyBST0MNCg0KYGBge3IsbWVzc2FnZT1GQUxTRX0NCiMgQ2FsY3VsYW1vcyBjdXJ2YXMgUk9DDQpyb2NfZnVsbCA8LSByb2MocmVzcG9uc2U9cHJlZGljdGlvbl9mdWxsJFN1cnZpdmVkLCBwcmVkaWN0b3I9cHJlZGljdGlvbl9mdWxsJC5maXR0ZWQpDQpyb2NfYmFkIDwtIHJvYyhyZXNwb25zZT1wcmVkaWN0aW9uX2JhZCRTdXJ2aXZlZCwgcHJlZGljdG9yPXByZWRpY3Rpb25fYmFkJC5maXR0ZWQpDQpgYGANCg0KR3JhZmljYW1vcyBhbWJhcyBlbiB1biBtaXNtbyBwbG90Lg0KDQpgYGB7cn0NCmdncm9jKGxpc3QoZnVsbD1yb2NfZnVsbCwgYmFkPXJvY19iYWQpLCBzaXplPTEpICsgDQogIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgaW50ZXJjZXB0ID0gMSwgbGluZXR5cGU9J2Rhc2hlZCcpICsNCiAgdGhlbWVfYncoKSArIA0KICBsYWJzKHRpdGxlPSdDdXJ2YXMgUk9DJywgY29sb3I9J01vZGVsbycpDQpwcmludChwYXN0ZSgnQVVDOiBNb2RlbG8gY29tcGxldG8nLCByb3VuZChyb2NfZnVsbCRhdWMsMykpKQ0KcHJpbnQocGFzdGUoJ0FVQzogTW9kZWxvIG1hbG8nLCByb3VuZChyb2NfYmFkJGF1YywzKSkpDQoNCmBgYA0KDQrCv1F1w6kgc2lnbmlmaWNhIGNhZGEgdW5vIGRlIGxvcyBlamVzPw0KDQojIyMgUHVudG8gZGUgY29ydGUNCg0KSGFzdGEgYWhvcmEgaGVtb3MgZXZhbHVhZG8gZWwgbW9kZWxvIGRlIG1hbmVyYSBnZW5lcmFsLCBwZXJvIGVsIHJlc3VsdGFkbyBmaW5hbCBkZWwgbW9kZWxvIGRlYmUgY29uc2lzdGlyIGVuIGFzaWduYXIgYSBsYSBwZXJzb25hIHVuYSBjbGFzZSBwcmVkaWNoYS4gRW4gbnVlc3RybyBjYXNvIGRlYmVtb3MgZXN0YWJsZWNlciB1biBwdW50byBkZSBjb3J0ZSBzZWfDum4gZWwgY3VhbCB2YW1vcyBhIHNlcGFyYXIgYSBsYXMgcGVyc29uYXMgZW4gcXVpZW5lcyBzb2JyZXZpdmVuIHkgcXVpZW5lcyBuby4NCg0KUHJvYmFtb3MgdmFyaW9zIHB1bnRvcyBkZSBjb3J0ZSB5IGdyYWZpY2Ftb3MgZWwgYWNjdXJhY3ksIGxhIHNlbnNpYmlsaWRhZCBvIHJlY2FsbCwgbGEgZXNwZWNpZmljaWRhZCB5IGxhIHByZWNpc2nDs24gcGFyYSBjYWRhIHVubyBkZSBlbGxvcy4NCg0KfCBDbGFzZXMgcHJlZGljaGFzIC8gQ2xhc2VzIHwgTmVnYXRpdmEgfCBQb3NpdGl2YSB8DQp8LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS18LS0tLS0tLS0tfC0tLS0tLS0tLS18DQp8IE5lZ2F0aXZhICAgICAgICAgICAgICAgICB8IFRydWUgTmVnIHwgRmFsc2UgTmVnIHwNCnwgUG9zaXRpdmEgICAgICAgICAgICAgICAgIHwgRmFsc2UgUG9zIHwgVHJ1ZSBQb3MgfA0KDQpSZWNvcmRlbW9zIHF1ZToNCg0KJGFjY3VyYWN5ID0gXGZyYWN7VFArVE59e1RQK0ZQK0ZOK1ROfSQNCg0KJHNlbnNpdGl2aXR5ID0gcmVjYWxsID0gXGZyYWN7VFB9e1RQK0ZOfSQNCg0KJHNwZWNpZmljaXR5ID0gXGZyYWN7VE59e1ROK0ZQfSQNCg0KJHByZWNpc2lvbiA9IFxmcmFje1RQfXtUUCtGUH0kDQoNCmBgYHtyfQ0KcHJlZGljdGlvbl9tZXRyaWNzIDwtIGZ1bmN0aW9uKGN1dG9mZiwgcHJlZGljdGlvbnM9cHJlZGljdGlvbl9mdWxsKXsNCiAgdGFiIDwtIHByZWRpY3Rpb25zICU+JSANCiAgICBtdXRhdGUocHJlZGljdGVkX2NsYXNzID0gaWZfZWxzZSguZml0dGVkID4gY3V0b2ZmLCAxLCAwKSwNCiAgICAgICAgICAgU3Vydml2ZWQgPSBmYWN0b3IoU3Vydml2ZWQpKQ0KICBjb25mdXNpb25NYXRyaXgodGFibGUodGFiJHByZWRpY3RlZF9jbGFzcywgdGFiJFN1cnZpdmVkKSwgcG9zaXRpdmUgPSAiMSIpICU+JQ0KICAgIHRpZHkoKSAlPiUNCiAgICBzZWxlY3QodGVybSwgZXN0aW1hdGUpICU+JQ0KICAgIGZpbHRlcih0ZXJtICVpbiUgYygnYWNjdXJhY3knLCAnc2Vuc2l0aXZpdHknLCAnc3BlY2lmaWNpdHknLCAncHJlY2lzaW9uJykpICU+JQ0KICAgIG11dGF0ZShjdXRvZmYgPSBjdXRvZmYpDQp9DQpjdXRvZmZzID0gc2VxKDAuMDUsMC45NSwwLjAxKQ0KbG9naXRfcHJlZCA9IG1hcF9kZihjdXRvZmZzLCBwcmVkaWN0aW9uX21ldHJpY3MpICU+JSANCiAgbXV0YXRlKHRlcm0gPSBhcy5mYWN0b3IodGVybSksIGVzdGltYXRlID0gcm91bmQoZXN0aW1hdGUsIDMpKQ0KZ2dwbG90KGxvZ2l0X3ByZWQsIGFlcyhjdXRvZmYsZXN0aW1hdGUsIGdyb3VwPXRlcm0sIGNvbG9yPXRlcm0pKSArIGdlb21fbGluZShzaXplPTEpICsNCiAgdGhlbWVfYncoKSArDQogIGxhYnModGl0bGU9ICdBY2N1cmFjeSwgU2Vuc2l0aXZpdHksIFNwZWNpZmljaXR5IHkgUHJlY2lzaW9uJywgc3VidGl0bGU9ICdNb2RlbG8gY29tcGxldG8nLCBjb2xvcj0iIikNCmBgYA0KDQrCv1F1w6kgcG9kZW1vcyBvYnNlcnZhciBlbiBlbCBncsOhZmljbz8NCg0Kwr9Qb2RlbW9zIGRlZmluaXIgdW4gYnVlbiBwdW50byBkZSBjb3J0ZT8gwr9DdcOhbCBzZXLDrWE/DQoNCsK/UG9yIHF1w6kgbGEgZXNwZWNpZmljaWRhZCB0aWVuZSBlc2UgY29tcG9ydGFtaWVudG8/DQoNCiMjIyBEYXRhc2V0IGRlIHRlc3RpbmcNCg0KU2VsZWNjaW9uYW1vcyBlbCBtb2RlbG8gY29tcGxldG8sIHlhIHF1ZSBlcyBlbCBxdWUgbWF4aW1pemFiYSBlbCBwb3JjZW50YWplIGRlIGRldmlhbmNlIGV4cGxpY2FkYSB5IGVuIGJhc2UgYSBsbyBxdWUgdmltb3MgZGVmaW5pbW9zIHVuIHB1bnRvIGRlIGNvcnRlIGVuIDAuNCAocHVlZGVuIHByb2JhciBvdHJvcyksIGRvbmRlIHNlIGNydXphbiBzZW5zaXRpdml0eSB5IHNwZWNpZmljaXR5Lg0KDQpDYWxjdWxhbW9zIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuIHBhcmEgbG9zIGRhdGFzZXRzIGRlIHRyYWluIHkgdGVzdC4NCg0KYGBge3IsbWVzc2FnZT1GQUxTRX0NCnNlbF9jdXRvZmYgPSAwLjQNCiMgQ3JlYW1vcyBlbCBtb2RlbG8NCmZ1bGxfbW9kZWwgPC0gZ2xtKGxvZ2l0X2Zvcm11bGFzJFBjU0FGLCBmYW1pbHkgPSAnYmlub21pYWwnLCBkYXRhID0gZGZ0aXRhbmljX3RyYWluKQ0KIyBjYWxjdWxhbW9zIGxhcyBwcmVkaWNjaW9uZXMgc29icmUgZWwgZGF0YXNldCBkZSB0cmFpbg0KdGFibGVfdHJhaW4gPSBhdWdtZW50KHggPSBmdWxsX21vZGVsLCB0eXBlLnByZWRpY3Q9J3Jlc3BvbnNlJykNCiMgQ2xhc2lmaWNhbW9zIHV0aWxpemFtb3MgZWwgcHVudG8gZGUgY29ydGUNCnRhYmxlX3RyYWluID0gdGFibGVfdHJhaW4gJT4lIA0KICBtdXRhdGUocHJlZGljdGVkX2NsYXNzID0gaWZfZWxzZSguZml0dGVkPnNlbF9jdXRvZmYsIDEsIDApICU+JSBhcy5mYWN0b3IoKSwgDQogICAgICAgICBTdXJ2aXZlZCA9IGZhY3RvcihTdXJ2aXZlZCkpDQojIENyZWFtb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24NCmNvbmZ1c2lvbk1hdHJpeCh0YWJsZSh0YWJsZV90cmFpbiRwcmVkaWN0ZWRfY2xhc3MsIHRhYmxlX3RyYWluJFN1cnZpdmVkKSwgcG9zaXRpdmUgPSAiMSIpDQpgYGANCg0KDQpgYGB7cixtZXNzYWdlPUZBTFNFfQ0KIyBBZ3JlZ2Ftb3MgbGEgcHJlZGljY2lvbmVzIGFsIGRhdGFzZXQgZGUgdGVzdGVvDQp0YWJsZV90ZXN0ID0gYXVnbWVudCh4ID0gZnVsbF9tb2RlbCwgbmV3ZGF0YT1kZnRpdGFuaWNfdGVzdCwgdHlwZS5wcmVkaWN0PSdyZXNwb25zZScpIA0KIyBDbGFzaWZpY2Ftb3MgdXRpbGl6YW1vcyBlbCBwdW50byBkZSBjb3J0ZQ0KdGFibGVfdGVzdCA9IHRhYmxlX3Rlc3QgJT4lIA0KICBtdXRhdGUocHJlZGljdGVkX2NsYXNzID0gaWZfZWxzZSguZml0dGVkPnNlbF9jdXRvZmYsIDEsIDApICU+JSBhcy5mYWN0b3IoKSwgDQogICAgICAgICBTdXJ2aXZlZCA9IGZhY3RvcihTdXJ2aXZlZCkpDQojIENyZWFtb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24NCmNvbmZ1c2lvbk1hdHJpeCh0YWJsZSh0YWJsZV90ZXN0JHByZWRpY3RlZF9jbGFzcywgdGFibGVfdGVzdCRTdXJ2aXZlZCksIHBvc2l0aXZlID0gIjEiKQ0KYGBgDQoNCkFsIGV4aXN0aXIgZGVzYmFsYW5jZW8gZGUgY2xhc2VzLCBlcyBwb3NpYmxlIHF1ZSBlbCBhbmFsaXN0YSBkZXNlZSBzYWJlciBzaSBsYSBwcmVjaXNpw7NuIGdlbmVyYWwgKEFjY3VyYWN5KSBkZSBzdSBtb2RlbG8gZXMgbWVqb3IgcXVlIGxhIHByb3BvcmNpw7NuIGRlIGRhdG9zIGNvbiBsYSBjbGFzZSBtYXlvcml0YXJpYSAoTm8taW5mb3JtYXRpb24gUmF0ZSkuIGNvbmZ1c2lvbk1hdHJpeCB1c2EgbGEgZnVuY2nDs24gYmlub20udGVzdCBwYXJhIHByb2JhciBxdWUgbGEgcHJlY2lzacOzbiAoQWNjKSBlcyBtZWpvciBxdWUgbGEgdGFzYSBzaW4gaW5mb3JtYWNpw7NuIChOSVIpLiBTaSBlbCBQLVZhbHVlIFtBY2MgPiBOSVJdIHJlc3VsdGEgc2lnbmlmaWNhdGl2bywgZW50b25jZXMgcG9kZW1vcyBkZWNpciBxdWUgZXhpc3RlIGV2aWRlbmNpYSBlc3RhZMOtc3RpY2FtZW50ZSBzaWduaWZpY2F0aXZhIHBhcmEgZGVjaXIgcXVlIGxhIHByZWNpc2nDs24gZGVsIG1vZGVsbyBlcyBtZWpvciBxdWUgbGEgdGFzYSBzaW4gaW5mb3JtYWNpw7NuLiANCg0KIyMgRGVzYmFsYW5jZW8gZGUgbGEgY2xhc2UNCg0KQWwgZXhwbG9yYXIgZWwgZGF0YXNldCB2aW1vcyBxdWUgZXhpc3TDrWEgY2llcnRvIGRlc2JhbGFuY2UgZGUgY2xhc2UuIEVzdG8gcHVlZGUgdGVuZXIgdW4gZWZlY3RvIGVuIGxhcyBlc3RpbWFjaW9uZXMgZGVsIG1vZGVsbyB5IHN1IGNsYXNpZmljYWNpw7NuIGZpbmFsLg0KDQpFeGlzdGVuIGRvcyBtYW5lcmFzIHNlbmNpbGxhcyBjb24gbGFzIGN1YWxlcyBwb2RlbW9zIHRyYWJhamFyIGNvbiB1bmEgY2xhc2UgZGVzYmFsYW5jZWFkYToNCg0KICAqIFNvYnJlLW11ZXN0cmVvIChvdmVyc2FtcGxpbmcpIGRlIGxhIGNsYXNlIG1pbm9yaXRhcmlhDQogIA0KICAqIFN1Yi1tdWVzdHJlbyAodW5kZXJzYW1wbGluZykgZGUgbGEgY2xhc2UgbWF5b3JpdGFyaWENCiAgDQpMYSBmdW5jacOzbiBgZ2xtYCBwdWVkZSB0b21hciBjb21vIGFyZ3VtZW50byB1bmEgY29sdW1uYSAoYHdlaWd0aHNgKSBkZSBwb25kZXJhZG9yZXMgcGFyYSBwb2RlciBoYWNlciBlc3RvLiBQb2RlbW9zIGFzaWduYXIgcGVzb3MgbWF5b3JlcyBhIDEgYSBsYSBjbGFzZSBtaW5vcml0YXJpYSAob3ZlcnNhbXBsaW5nKSBvIG1lbm9yZXMgYSAxIGEgbGEgY2xhc2UgbWF5b3JpdGFyaWEgKHVuZGVyc2FtcGxpbmcpLiBFbiBudWVzdHJvIHByb2JsZW1hIHZhbW9zIGEgcmVhbGl6YXIgdW4gc29icmVzYW1wbGVvIGRlIGxhIGNsYXNlIG1pbm9yaXRhcmlhLg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCiMgQ3JlYW1vcyBsYSBjb2x1bW5hIGRlIHBvbmRlcmFkb3Jlcw0KdHJhaW4gPC0gZGZ0aXRhbmljX3RyYWluICU+JSBtdXRhdGUod3QgPSBpZl9lbHNlKFN1cnZpdmVkID09IDEsIDEuMSwgMSkpDQojIENyZWFtb3MgbG9zIG1vZGVsb3MgY29uIGxhIGRhdGEgJ2JhbGFuY2VhZGEnDQpiYWxhbmNlZF9tb2RlbHMgPC0gZGF0YV9mcmFtZShsb2dpdF9mb3JtdWxhcykgJT4lICMgZGF0YWZyYW1lIGEgcGFydGlyIGRlbCBvYmpldG8gZm9ybXVsYXMNCiAgbXV0YXRlKG1vZGVscyA9IG5hbWVzKGxvZ2l0X2Zvcm11bGFzKSwgIyBjb2x1bW5hIGNvbiBsb3Mgbm9tYnJlcyBkZSBsYXMgZm9ybXVsYXMNCiAgICAgICAgIGV4cHJlc3Npb24gPSBwYXN0ZShsb2dpdF9mb3JtdWxhcyksICMgY29sdW1uYSBjb24gbGFzIGV4cHJlc2lvbmVzIGRlIGxhcyBmb3JtdWxhcw0KICAgICAgICAgbW9kID0gbWFwKGxvZ2l0X2Zvcm11bGFzLCB+Z2xtKC4sIGZhbWlseSA9ICdiaW5vbWlhbCcsIGRhdGEgPSB0cmFpbiwgd2VpZ2h0cyA9IHd0KSkpICNQYXNhbW9zIGxhIGNvbHVtbmEgd3QgY29tbyBwb25kZXJhZG9yZXMNCmBgYA0KDQpWZW1vcyBsYXMgZXN0aW1hY2lvbmVzIGRlIGxvcyBwYXLDoW1ldHJvcyBwYXJhIGVsIG1vZGVsbyBjb21wbGV0by4gwr9FeGlzdGVuIGNhbWJpb3M/DQoNCmBgYHtyLCAgd2FybmluZz1GQUxTRSwgZWNobz1GQUxTRX0NCmJhbGFuY2VkX21vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHMgPT0gIlBjU0FGIikgJT4lDQogIG11dGF0ZSh0aWR5ID0gbWFwKG1vZCx0aWR5KSkgJT4lDQogIHVubmVzdCh0aWR5LCAuZHJvcCA9IFRSVUUpICU+JSANCiAgbXV0YXRlKGVzdGltYXRlPXJvdW5kKGVzdGltYXRlLDUpLA0KICAgICAgICAgcC52YWx1ZT1yb3VuZChwLnZhbHVlLDQpKQ0KYGBgDQoNCkFob3JhIHZlYW1vcyBsYSBldmFsdWFjacOzbiBkZSBsb3MgbW9kZWxvcyDCv1F1w6kgcGFzw7MgY29uIGVsIHBvcmNlbnRhamUgZGUgZGV2aWFuY2UgZXhwbGljYWRhPyDCv1kgY29uIGxhIG51bGE/DQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KYmFsYW5jZWRfbW9kZWxzIDwtIGJhbGFuY2VkX21vZGVscyAlPiUgDQogIG11dGF0ZShnbGFuY2UgPSBtYXAobW9kLGdsYW5jZSkpDQpiYWxhbmNlZF9tb2RlbHMgJT4lIA0KICB1bm5lc3QoZ2xhbmNlLCAuZHJvcCA9IFRSVUUpICU+JQ0KICBtdXRhdGUocGVyY19leHBsYWluZWRfZGV2ID0gMS1kZXZpYW5jZS9udWxsLmRldmlhbmNlKSAlPiUgDQogIHNlbGVjdCgtYyhtb2RlbHMsIGRmLm51bGwsIEFJQywgQklDKSkgJT4lIA0KICBhcnJhbmdlKGRldmlhbmNlKQ0KYGBgDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KYmFsYW5jZWRfbW9kZWxzIDwtIGJhbGFuY2VkX21vZGVscyAlPiUgDQogIG11dGF0ZShwcmVkPSBtYXAobW9kLGF1Z21lbnQsIHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpKQ0KcHJlZGljdGlvbl9mdWxsIDwtIGJhbGFuY2VkX21vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHM9PSJQY1NBRiIpICU+JSANCiAgdW5uZXN0KHByZWQsIC5kcm9wPVRSVUUpDQpyb2NfZnVsbCA8LSByb2MocmVzcG9uc2U9cHJlZGljdGlvbl9mdWxsJFN1cnZpdmVkLCBwcmVkaWN0b3I9cHJlZGljdGlvbl9mdWxsJC5maXR0ZWQpDQpwcmVkaWN0aW9uX2JhZCA8LSBiYWxhbmNlZF9tb2RlbHMgJT4lIA0KICBmaWx0ZXIobW9kZWxzPT0iYWdlIikgJT4lIA0KICB1bm5lc3QocHJlZCwgLmRyb3A9VFJVRSkNCnJvY19iYWQgPC0gcm9jKHJlc3BvbnNlPXByZWRpY3Rpb25fYmFkJFN1cnZpdmVkLCBwcmVkaWN0b3I9cHJlZGljdGlvbl9iYWQkLmZpdHRlZCkNCmBgYA0KDQojIyMgVmlvbGluIHBsb3RzLCBDdXJ2YXMgUk9DIHkgQVVDcw0KDQpSZWFsaXphbW9zIGxvcyBncsOhZmljb3MgZGUgdmlvbGluLCBsYXMgY3VydmFzIFJPQyB5IGNhbGN1bGFtb3MgbGFzIEFVQy4NCg0KYGBge3IsIHdhcm5pbmc9RiwgZWNobz1GQUxTRX0NCnZpb2xpbl9mdWxsID0gZ2dwbG90KHByZWRpY3Rpb25fZnVsbCwgYWVzKHggPSBTdXJ2aXZlZCwgeSA9LmZpdHRlZCwgZ3JvdXAgPSBTdXJ2aXZlZCwgZmlsbCA9IGZhY3RvcihTdXJ2aXZlZCkpKSArIA0KICBnZW9tX3Zpb2xpbigpICsNCiAgdGhlbWVfYncoKSArDQogIGd1aWRlcyhmaWxsPUZBTFNFKSArDQogIGxhYnModGl0bGU9J1Zpb2xpbiBwbG90Jywgc3VidGl0bGU9J01vZGVsbyBjb21wbGV0bycsIHk9J1ByZWRpY3RlZCBwcm9iYWJpbGl0eScpDQoNCnZpb2xpbl9iYWQ9Z2dwbG90KHByZWRpY3Rpb25fYmFkLCBhZXMoeCA9IFN1cnZpdmVkLCB5ID0uZml0dGVkLCBncm91cCA9IFN1cnZpdmVkLCBmaWxsID0gZmFjdG9yKFN1cnZpdmVkKSkpICsgDQogIGdlb21fdmlvbGluKCkgKyANCiAgdGhlbWVfYncoKSArDQogIGd1aWRlcyhmaWxsPUZBTFNFKSArDQogIGxhYnModGl0bGU9J1Zpb2xpbiBwbG90Jywgc3VidGl0bGU9J01vZGVsbyBtYWxvJywgeT0nUHJlZGljdGVkIHByb2JhYmlsaXR5JykNCg0KcGxvdF9ncmlkKHZpb2xpbl9iYWQsIHZpb2xpbl9mdWxsKQ0KDQpnZ3JvYyhsaXN0KGZ1bGw9cm9jX2Z1bGwsIGJhZD1yb2NfYmFkKSwgc2l6ZT0xKSArIGdlb21fYWJsaW5lKHNsb3BlID0gMSwgaW50ZXJjZXB0ID0gMSwgbGluZXR5cGU9J2Rhc2hlZCcpICsgdGhlbWVfYncoKSArIGxhYnModGl0bGU9J0N1cnZhcyBST0MnLCBjb2xvcj0nTW9kZWxvJykNCg0KcHJpbnQocGFzdGUoJ0FVQyBNb2RlbG8gY29tcGxldG86Jywgcm91bmQocm9jX2Z1bGwkYXVjLDMpKSkNCg0KcHJpbnQocGFzdGUoJ0FVQyBNb2RlbG8gbWFsbzonLCByb3VuZChyb2NfYmFkJGF1YywzKSkpDQoNCmBgYA0KDQo+IMK/RMOzbmRlIHNlIHZlbiBsb3MgY2FtYmlvcyBtw6FzIG5vdG9yaW9zIHJlc3BlY3RvIGEgbnVlc3Ryb3MgbW9kZWxvcyBhbnRlcmlvcmVzIHF1ZSBubyB0ZW7DrWFuIGVuIGN1ZW50YSBlbCBkZXNiYWxhbmNlIGRlIGxhIGNsYXNlPw0KDQojIyMgUHVudG8gZGUgY29ydGUNCg0KVm9sdmVtb3MgYSByZWFsaXphciBsYXMgcHJ1ZWJhcyBwYXJhIHZhcmlvcyBwdW50b3MgZGUgY29ydGUgeSBncmFmaWNhbW9zIGVsIGFjY3VyYWN5LCBsYSBzZW5zaWJpbGlkYWQsIGxhIGVzcGVjaWZpY2lkYWQsIGVsIHJlY2FsbCB5IGxhIHByZWNpc2lvbiBwYXJhIGNhZGEgdW5vIGRlIGVsbG9zLg0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmN1dG9mZnMgPSBzZXEoMC4wNSwwLjk1LDAuMDEpDQpsb2dpdF9wcmVkPSBtYXBfZGZyKGN1dG9mZnMsIHByZWRpY3Rpb25fbWV0cmljcyklPiUgbXV0YXRlKHRlcm09YXMuZmFjdG9yKHRlcm0pKQ0KDQpnZ3Bsb3QobG9naXRfcHJlZCwgYWVzKGN1dG9mZixlc3RpbWF0ZSwgZ3JvdXA9dGVybSwgY29sb3I9dGVybSkpICsgZ2VvbV9saW5lKHNpemU9MSkgKw0KICB0aGVtZV9idygpICsNCiAgbGFicyh0aXRsZT0gJ0FjY3VyYWN5LCBQcmVjaXNpb24sIFNlbnNpdGl2aXR5IHkgU3BlY2lmaWNpdHknLCBzdWJ0aXRsZT0gJ01vZGVsbyBjb21wbGV0bycsIGNvbG9yPSIiKQ0KYGBgDQoNCsK/UXXDqSBjYW1iaW9zIHZlbW9zIHJlc3BlY3RvIGFsIGdyw6FmaWNvIGFudGVyaW9yPw0KDQojIyMgRGF0YXNldCBkZSB0ZXN0aW5nDQoNClByb2JhbW9zIGVuIGVsIGRhdGFzZXQgZGUgdGVzdGluZyBudWVzdHJvIG1vZGVsbyBiYWxhbmNlYWRvLiBObyBlcyBuZWNlc2FyaW8gcXVlIGxlIGNyZWVtb3MgcGVzb3MgYWwgZGF0YXNldCBkZSB0ZXN0ZW8uDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KZnVsbF9tb2RlbCA8LSBnbG0obG9naXRfZm9ybXVsYXMkUGNTQUYsIGZhbWlseSA9ICdiaW5vbWlhbCcsIGRhdGEgPSB0cmFpbiwgd2VpZ2h0cyA9IHd0KQ0KDQp0YWJsZT0gYXVnbWVudCh4PWZ1bGxfbW9kZWwsIG5ld2RhdGE9ZGZ0aXRhbmljX3Rlc3QsIHR5cGUucHJlZGljdD0ncmVzcG9uc2UnKSANCg0KdGFibGU9dGFibGUgJT4lIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPjAuNDIsIDEsIDApICU+JSBhcy5mYWN0b3IoKSwNCiAgICAgICAgICAgU3Vydml2ZWQgPSBmYWN0b3IoU3Vydml2ZWQpKQ0KDQpjb25mdXNpb25NYXRyaXgodGFibGUodGFibGUkcHJlZGljdGVkX2NsYXNzLCB0YWJsZSRTdXJ2aXZlZCksIHBvc2l0aXZlID0gIjEiKQ0KYGBgDQoNCg0K